From e0f450ab47b58f43ffdaa349f01d816fa7558d4a Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 7 Mar 2013 17:23:05 -0300 Subject: [PATCH 001/397] Move source files to proper subdirectory. --- util.py | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 util.py diff --git a/util.py b/util.py new file mode 100644 index 0000000..c64d4c5 --- /dev/null +++ b/util.py @@ -0,0 +1,187 @@ +""" +Utilities for Soledad. +""" + +import os +import gnupg +import re +from gnupg import ( + logger, + _is_sequence, + _make_binary_stream, +) + + +class ListPackets(): + """ + Handle status messages for --list-packets. + """ + + def __init__(self, gpg): + self.gpg = gpg + self.nodata = None + self.key = None + self.need_passphrase = None + self.need_passphrase_sym = None + self.userid_hint = None + + def handle_status(self, key, value): + # TODO: write tests for handle_status + if key == 'NODATA': + self.nodata = True + if key == 'ENC_TO': + # This will only capture keys in our keyring. In the future we + # may want to include multiple unknown keys in this list. + self.key, _, _ = value.split() + if key == 'NEED_PASSPHRASE': + self.need_passphrase = True + if key == 'NEED_PASSPHRASE_SYM': + self.need_passphrase_sym = True + if key == 'USERID_HINT': + self.userid_hint = value.strip().split() + + +class GPGWrapper(gnupg.GPG): + """ + This is a temporary class for handling GPG requests, and should be + replaced by a more general class used throughout the project. + """ + + GNUPG_HOME = os.environ['HOME'] + "/.config/leap/gnupg" + GNUPG_BINARY = "/usr/bin/gpg" # this has to be changed based on OS + + def __init__(self, gpgbinary=GNUPG_BINARY, gnupghome=GNUPG_HOME, + verbose=False, use_agent=False, keyring=None, options=None): + super(GPGWrapper, self).__init__(gnupghome=gnupghome, + gpgbinary=gpgbinary, + verbose=verbose, + use_agent=use_agent, + keyring=keyring, + options=options) + self.result_map['list-packets'] = ListPackets + + def find_key_by_email(self, email, secret=False): + """ + Find user's key based on their email. + """ + for key in self.list_keys(secret=secret): + for uid in key['uids']: + if re.search(email, uid): + return key + raise LookupError("GnuPG public key for email %s not found!" % email) + + def find_key_by_subkey(self, subkey): + for key in self.list_keys(): + for sub in key['subkeys']: + if sub[0] == subkey: + return key + raise LookupError( + "GnuPG public key for subkey %s not found!" % subkey) + + def find_key_by_keyid(self, keyid): + for key in self.list_keys(): + if keyid == key['keyid']: + return key + raise LookupError( + "GnuPG public key for subkey %s not found!" % subkey) + + def encrypt(self, data, recipient, sign=None, always_trust=True, + passphrase=None, symmetric=False): + """ + Encrypt data using GPG. + """ + # TODO: devise a way so we don't need to "always trust". + return super(GPGWrapper, self).encrypt(data, recipient, sign=sign, + always_trust=always_trust, + passphrase=passphrase, + symmetric=symmetric, + cipher_algo='AES256') + + def decrypt(self, data, always_trust=True, passphrase=None): + """ + Decrypt data using GPG. + """ + # TODO: devise a way so we don't need to "always trust". + return super(GPGWrapper, self).decrypt(data, + always_trust=always_trust, + passphrase=passphrase) + + def send_keys(self, keyserver, *keyids): + """ + Send keys to a keyserver + """ + result = self.result_map['list'](self) + gnupg.logger.debug('send_keys: %r', keyids) + data = gnupg._make_binary_stream("", self.encoding) + args = ['--keyserver', keyserver, '--send-keys'] + args.extend(keyids) + self._handle_io(args, data, result, binary=True) + gnupg.logger.debug('send_keys result: %r', result.__dict__) + data.close() + return result + + def encrypt_file(self, file, recipients, sign=None, + always_trust=False, passphrase=None, + armor=True, output=None, symmetric=False, + cipher_algo=None): + "Encrypt the message read from the file-like object 'file'" + args = ['--encrypt'] + if symmetric: + args = ['--symmetric'] + if cipher_algo: + args.append('--cipher-algo %s' % cipher_algo) + else: + args = ['--encrypt'] + if not _is_sequence(recipients): + recipients = (recipients,) + for recipient in recipients: + args.append('--recipient "%s"' % recipient) + if armor: # create ascii-armored output - set to False for binary + args.append('--armor') + if output: # write the output to a file with the specified name + if os.path.exists(output): + os.remove(output) # to avoid overwrite confirmation message + args.append('--output "%s"' % output) + if sign: + args.append('--sign --default-key "%s"' % sign) + if always_trust: + args.append("--always-trust") + result = self.result_map['crypt'](self) + self._handle_io(args, file, result, passphrase=passphrase, binary=True) + logger.debug('encrypt result: %r', result.data) + return result + + def list_packets(self, raw_data): + args = ["--list-packets"] + result = self.result_map['list-packets'](self) + self._handle_io( + args, + _make_binary_stream(raw_data, self.encoding), + result, + ) + return result + + def encrypted_to(self, raw_data): + """ + Return the key to which raw_data is encrypted to. + """ + # TODO: make this support multiple keys. + result = self.list_packets(raw_data) + if not result.key: + raise LookupError( + "Content is not encrypted to a GnuPG key!") + try: + return self.find_key_by_keyid(result.key) + except: + return self.find_key_by_subkey(result.key) + + def is_encrypted_sym(self, raw_data): + result = self.list_packets(raw_data) + return bool(result.need_passphrase_sym) + + def is_encrypted_asym(self, raw_data): + result = self.list_packets(raw_data) + return bool(result.key) + + def is_encrypted(self, raw_data): + self.is_encrypted_asym() or self.is_encrypted_sym() From 2953bad41ed85ea60c219dd5e89088823bfb121d Mon Sep 17 00:00:00 2001 From: drebs Date: Sun, 10 Mar 2013 15:58:15 -0300 Subject: [PATCH 002/397] Add namespace package. From fbff6fb6ddace0316d6f48008a579f707a144527 Mon Sep 17 00:00:00 2001 From: drebs Date: Sun, 10 Mar 2013 17:11:50 -0300 Subject: [PATCH 003/397] Remove monkeypatch logging for now. From 58fd7bedac08e6692819b3044436c0640f00d8b5 Mon Sep 17 00:00:00 2001 From: drebs Date: Sun, 10 Mar 2013 18:45:01 -0300 Subject: [PATCH 004/397] Add packages to namespace. From faadc426cf0ac58572217256cc5794d359a8168c Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 11 Mar 2013 12:20:11 -0300 Subject: [PATCH 005/397] Add recovery document export. From 37f70cd98622c41a67be2fde4e7200ca97168e8d Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 11 Mar 2013 13:14:14 -0300 Subject: [PATCH 006/397] Add import recovery document. From 86be391a56257bcc399699a4f4a818b222a2cecb Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 5 Apr 2013 22:50:52 +0000 Subject: [PATCH 007/397] Add documentation creator and first set of API docs. * This uses Sphinx's command 'sphinx-apidoc'. * TODO Find a way to make Sphinx build the documentations without installing the module locally first. --- Makefile | 4 + docs/Makefile | 153 + docs/_build/doctrees/environment.pickle | Bin 0 -> 5529 bytes docs/_build/doctrees/gnupg.doctree | Bin 0 -> 2844 bytes docs/_build/doctrees/index.doctree | Bin 0 -> 5533 bytes docs/_build/doctrees/setup.doctree | Bin 0 -> 2844 bytes docs/_build/doctrees/test_gnupg.doctree | Bin 0 -> 2884 bytes docs/_build/html/.buildinfo | 4 + docs/_build/html/_sources/gnupg.txt | 7 + docs/_build/html/_sources/index.txt | 25 + docs/_build/html/_sources/setup.txt | 7 + docs/_build/html/_sources/test_gnupg.txt | 7 + docs/_build/html/_static/ajax-loader.gif | Bin 0 -> 673 bytes docs/_build/html/_static/basic.css | 540 ++ docs/_build/html/_static/comment-bright.png | Bin 0 -> 3500 bytes docs/_build/html/_static/comment-close.png | Bin 0 -> 3578 bytes docs/_build/html/_static/comment.png | Bin 0 -> 3445 bytes docs/_build/html/_static/default.css | 256 + docs/_build/html/_static/doctools.js | 247 + docs/_build/html/_static/down-pressed.png | Bin 0 -> 368 bytes docs/_build/html/_static/down.png | Bin 0 -> 363 bytes docs/_build/html/_static/file.png | Bin 0 -> 392 bytes docs/_build/html/_static/jquery.js | 9404 +++++++++++++++++++ docs/_build/html/_static/minus.png | Bin 0 -> 199 bytes docs/_build/html/_static/plus.png | Bin 0 -> 199 bytes docs/_build/html/_static/pygments.css | 62 + docs/_build/html/_static/searchtools.js | 560 ++ docs/_build/html/_static/sidebar.js | 151 + docs/_build/html/_static/underscore.js | 1226 +++ docs/_build/html/_static/up-pressed.png | Bin 0 -> 372 bytes docs/_build/html/_static/up.png | Bin 0 -> 363 bytes docs/_build/html/_static/websupport.js | 808 ++ docs/_build/html/genindex.html | 95 + docs/_build/html/gnupg.html | 112 + docs/_build/html/index.html | 126 + docs/_build/html/objects.inv | Bin 0 -> 210 bytes docs/_build/html/search.html | 99 + docs/_build/html/searchindex.js | 1 + docs/_build/html/setup.html | 112 + docs/_build/html/test_gnupg.html | 102 + docs/conf.py | 286 + docs/gnupg.rst | 7 + docs/index.rst | 25 + docs/make.bat | 190 + docs/setup.rst | 7 + docs/test_gnupg.rst | 7 + 46 files changed, 14630 insertions(+) create mode 100644 docs/Makefile create mode 100644 docs/_build/doctrees/environment.pickle create mode 100644 docs/_build/doctrees/gnupg.doctree create mode 100644 docs/_build/doctrees/index.doctree create mode 100644 docs/_build/doctrees/setup.doctree create mode 100644 docs/_build/doctrees/test_gnupg.doctree create mode 100644 docs/_build/html/.buildinfo create mode 100644 docs/_build/html/_sources/gnupg.txt create mode 100644 docs/_build/html/_sources/index.txt create mode 100644 docs/_build/html/_sources/setup.txt create mode 100644 docs/_build/html/_sources/test_gnupg.txt create mode 100644 docs/_build/html/_static/ajax-loader.gif create mode 100644 docs/_build/html/_static/basic.css create mode 100644 docs/_build/html/_static/comment-bright.png create mode 100644 docs/_build/html/_static/comment-close.png create mode 100644 docs/_build/html/_static/comment.png create mode 100644 docs/_build/html/_static/default.css create mode 100644 docs/_build/html/_static/doctools.js create mode 100644 docs/_build/html/_static/down-pressed.png create mode 100644 docs/_build/html/_static/down.png create mode 100644 docs/_build/html/_static/file.png create mode 100644 docs/_build/html/_static/jquery.js create mode 100644 docs/_build/html/_static/minus.png create mode 100644 docs/_build/html/_static/plus.png create mode 100644 docs/_build/html/_static/pygments.css create mode 100644 docs/_build/html/_static/searchtools.js create mode 100644 docs/_build/html/_static/sidebar.js create mode 100644 docs/_build/html/_static/underscore.js create mode 100644 docs/_build/html/_static/up-pressed.png create mode 100644 docs/_build/html/_static/up.png create mode 100644 docs/_build/html/_static/websupport.js create mode 100644 docs/_build/html/genindex.html create mode 100644 docs/_build/html/gnupg.html create mode 100644 docs/_build/html/index.html create mode 100644 docs/_build/html/objects.inv create mode 100644 docs/_build/html/search.html create mode 100644 docs/_build/html/searchindex.js create mode 100644 docs/_build/html/setup.html create mode 100644 docs/_build/html/test_gnupg.html create mode 100644 docs/conf.py create mode 100644 docs/gnupg.rst create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/setup.rst create mode 100644 docs/test_gnupg.rst diff --git a/Makefile b/Makefile index c765da6..1eb8024 100644 --- a/Makefile +++ b/Makefile @@ -16,3 +16,7 @@ test: cleantest install: python setup.py install +docs: + sphinx-apidoc -o docs -F -A "Isis Agora Lovecruft" -H "python-gnupg" -V 0.3.1 -R 0.3.1 . + cd docs + make html diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..7159d3e --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,153 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-gnupg.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-gnupg.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/python-gnupg" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-gnupg" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/_build/doctrees/environment.pickle b/docs/_build/doctrees/environment.pickle new file mode 100644 index 0000000000000000000000000000000000000000..8d2c9d7140c5359df1409329e8bc97777d69daf2 GIT binary patch literal 5529 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 literal 0 HcmV?d00001 diff --git a/docs/_build/doctrees/index.doctree b/docs/_build/doctrees/index.doctree new file mode 100644 index 0000000000000000000000000000000000000000..95318c2ef1b356260adaf56a539098ab5dadf836 GIT binary patch literal 5533 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 literal 0 HcmV?d00001 diff --git a/docs/_build/html/_static/basic.css b/docs/_build/html/_static/basic.css new file mode 100644 index 0000000..43e8baf --- /dev/null +++ b/docs/_build/html/_static/basic.css @@ -0,0 +1,540 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox input[type="text"] { + width: 170px; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + width: 30px; +} + +img { + border: 0; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li div.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable dl, table.indextable dd { + margin-top: 0; + margin-bottom: 0; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- general body styles --------------------------------------------------- */ + +a.headerlink { + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.field-list ul { + padding-left: 1em; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px 7px 0 7px; + background-color: #ffe; + width: 40%; + float: right; +} + +p.sidebar-title { + font-weight: bold; +} + +/* -- topics ---------------------------------------------------------------- */ + +div.topic { + border: 1px solid #ccc; + padding: 7px 7px 0 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +div.admonition dl { + margin-bottom: 0; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + border: 0; + border-collapse: collapse; +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.field-list td, table.field-list th { + border: 0 !important; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +dl { + margin-bottom: 15px; +} + +dd p { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dt:target, .highlighted { + background-color: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.refcount { + color: #060; +} + +.optional { + font-size: 1.3em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +td.linenos pre { + padding: 5px 0px; + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + margin-left: 0.5em; +} + +table.highlighttable td { + padding: 0 0.5em 0 0.5em; +} + +tt.descname { + background-color: transparent; + font-weight: bold; + font-size: 1.2em; +} + +tt.descclassname { + background-color: transparent; +} + +tt.xref, a tt { + background-color: transparent; + font-weight: bold; +} + +h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/docs/_build/html/_static/comment-bright.png b/docs/_build/html/_static/comment-bright.png new file mode 100644 index 0000000000000000000000000000000000000000..551517b8c83b76f734ff791f847829a760ad1903 GIT binary patch literal 3500 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 A2LJ#7 literal 0 HcmV?d00001 diff --git a/docs/_build/html/_static/comment.png b/docs/_build/html/_static/comment.png new file mode 100644 index 0000000000000000000000000000000000000000..92feb52b8824c6b0f59b658b1196c61de9162a95 GIT binary patch literal 3445 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 Xn!ZpXD%P(H00000NkvXXu0mjf= 0 && !jQuery(node.parentNode).hasClass(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.initIndexTable(); + }, + + /** + * 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 safely 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() { + $('\u00B6'). + attr('href', '#' + this.id). + attr('title', _('Permalink to this headline')). + appendTo(this); + }); + $('dt[id]').each(function() { + $('\u00B6'). + 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(), 'highlighted'); + }); + }, 10); + $('') + .appendTo($('#searchbox')); + } + }, + + /** + * init the domain index toggle buttons + */ + initIndexTable : function() { + var togglers = $('img.toggler').click(function() { + var src = $(this).attr('src'); + var idnum = $(this).attr('id').substr(7); + $('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_INDEX) { + togglers.click(); + } + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords : function() { + $('#searchbox .highlight-link').fadeOut(300); + $('span.highlighted').removeClass('highlighted'); + }, + + /** + * 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(); +}); diff --git a/docs/_build/html/_static/down-pressed.png b/docs/_build/html/_static/down-pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..6f7ad782782e4f8e39b0c6e15c7344700cdd2527 GIT binary patch literal 368 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6U4S$Y z{B+)352QE?JR*yM+OLB!qm#z$3ZNi+iKnkC`z>}Z23@f-Ava~9&<9T!#}JFtXD=!G zGdl{fK6ro2OGiOl+hKvH6i=D3%%Y^j`yIkRn!8O>@bG)IQR0{Kf+mxNd=_WScA8u_ z3;8(7x2){m9`nt+U(Nab&1G)!{`SPVpDX$w8McLTzAJ39wprG3p4XLq$06M`%}2Yk zRPPsbES*dnYm1wkGL;iioAUB*Or2kz6(-M_r_#Me-`{mj$Z%( literal 0 HcmV?d00001 diff --git a/docs/_build/html/_static/down.png b/docs/_build/html/_static/down.png new file mode 100644 index 0000000000000000000000000000000000000000..3003a88770de3977d47a2ba69893436a2860f9e7 GIT binary patch literal 363 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6U4S$Y z{B+)352QE?JR*yM+OLB!qm#z$3ZNi+iKnkC`z>}xaV3tUZ$qnrLa#kt978NlpS`ru z&)HFc^}^>{UOEce+71h5nn>6&w6A!ieNbu1wh)UGh{8~et^#oZ1# z>T7oM=FZ~xXWnTo{qnXm$ZLOlqGswI_m2{XwVK)IJmBjW{J3-B3x@C=M{ShWt#fYS9M?R;8K$~YwlIqwf>VA7q=YKcwf2DS4Zj5inDKXXB1zl=(YO3ST6~rDq)&z z*o>z)=hxrfG-cDBW0G$!?6{M<$@{_4{m1o%Ub!naEtn|@^frU1tDnm{r-UW|!^@B8 literal 0 HcmV?d00001 diff --git a/docs/_build/html/_static/file.png b/docs/_build/html/_static/file.png new file mode 100644 index 0000000000000000000000000000000000000000..d18082e397e7e54f20721af768c4c2983258f1b4 GIT binary patch literal 392 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmP$HyOL$D9)yc9|lc|nKf<9@eUiWd>3GuTC!a5vdfWYEazjncPj5ZQX%+1 zt8B*4=d)!cdDz4wr^#OMYfqGz$1LDFF>|#>*O?AGil(WEs?wLLy{Gj2J_@opDm%`dlax3yA*@*N$G&*ukFv>P8+2CBWO(qz zD0k1@kN>hhb1_6`&wrCswzINE(evt-5C1B^STi2@PmdKI;Vst0PQB6!2kdN literal 0 HcmV?d00001 diff --git a/docs/_build/html/_static/jquery.js b/docs/_build/html/_static/jquery.js new file mode 100644 index 0000000..96d660c --- /dev/null +++ b/docs/_build/html/_static/jquery.js @@ -0,0 +1,9404 @@ +/*! + * jQuery JavaScript Library v1.7.2 + * http://jquery.com/ + * + * Copyright 2011, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Thu Nov 15 18:28:24 BRST 2012 + */ +(function( window, undefined ) { + +// Use the correct document accordingly with window argument (sandbox) +var document = window.document, + navigator = window.navigator, + location = window.location; +var jQuery = (function() { + +// Define a local copy of jQuery +var jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context, rootjQuery ); + }, + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$, + + // A central reference to the root jQuery(document) + rootjQuery, + + // A simple way to check for HTML strings or ID strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/, + + // Check if a string has a non-whitespace character in it + rnotwhite = /\S/, + + // Used for trimming whitespace + trimLeft = /^\s+/, + trimRight = /\s+$/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + + // Useragent RegExp + rwebkit = /(webkit)[ \/]([\w.]+)/, + ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/, + rmsie = /(msie) ([\w.]+)/, + rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, + + // Matches dashed string for camelizing + rdashAlpha = /-([a-z]|[0-9])/ig, + rmsPrefix = /^-ms-/, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return ( letter + "" ).toUpperCase(); + }, + + // Keep a UserAgent string for use with jQuery.browser + userAgent = navigator.userAgent, + + // For matching the engine and version of the browser + browserMatch, + + // The deferred used on DOM ready + readyList, + + // The ready event handler + DOMContentLoaded, + + // Save a reference to some core methods + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty, + push = Array.prototype.push, + slice = Array.prototype.slice, + trim = String.prototype.trim, + indexOf = Array.prototype.indexOf, + + // [[Class]] -> type pairs + class2type = {}; + +jQuery.fn = jQuery.prototype = { + constructor: jQuery, + init: function( selector, context, rootjQuery ) { + var match, elem, ret, doc; + + // Handle $(""), $(null), or $(undefined) + if ( !selector ) { + return this; + } + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + } + + // The body element only exists once, optimize finding it + if ( selector === "body" && !context && document.body ) { + this.context = document; + this[0] = document.body; + this.selector = selector; + this.length = 1; + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + // Are we dealing with HTML string or an ID? + if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = quickExpr.exec( selector ); + } + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + doc = ( context ? context.ownerDocument || context : document ); + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + ret = rsingleTag.exec( selector ); + + if ( ret ) { + if ( jQuery.isPlainObject( context ) ) { + selector = [ document.createElement( ret[1] ) ]; + jQuery.fn.attr.call( selector, context, true ); + + } else { + selector = [ doc.createElement( ret[1] ) ]; + } + + } else { + ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); + selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes; + } + + return jQuery.merge( this, selector ); + + // HANDLE: $("#id") + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || rootjQuery ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return rootjQuery.ready( selector ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.7.2", + + // The default length of a jQuery object is 0 + length: 0, + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + toArray: function() { + return slice.call( this, 0 ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == null ? + + // Return a 'clean' array + this.toArray() : + + // Return just the object + ( num < 0 ? this[ this.length + num ] : this[ num ] ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + // Build a new jQuery matched element set + var ret = this.constructor(); + + if ( jQuery.isArray( elems ) ) { + push.apply( ret, elems ); + + } else { + jQuery.merge( ret, elems ); + } + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) { + ret.selector = this.selector + ( this.selector ? " " : "" ) + selector; + } else if ( name ) { + ret.selector = this.selector + "." + name + "(" + selector + ")"; + } + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + ready: function( fn ) { + // Attach the listeners + jQuery.bindReady(); + + // Add the callback + readyList.add( fn ); + + return this; + }, + + eq: function( i ) { + i = +i; + return i === -1 ? + this.slice( i ) : + this.slice( i, i + 1 ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ), + "slice", slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: [].sort, + splice: [].splice +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function( deep ) { + if ( window.$ === jQuery ) { + window.$ = _$; + } + + if ( deep && window.jQuery === jQuery ) { + window.jQuery = _jQuery; + } + + return jQuery; + }, + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + // Either a released hold or an DOMready/load event and not yet ready + if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready, 1 ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.fireWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.trigger ) { + jQuery( document ).trigger( "ready" ).off( "ready" ); + } + } + }, + + bindReady: function() { + if ( readyList ) { + return; + } + + readyList = jQuery.Callbacks( "once memory" ); + + // Catch cases where $(document).ready() is called after the + // browser event has already occurred. + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + return setTimeout( jQuery.ready, 1 ); + } + + // Mozilla, Opera and webkit nightlies currently support this event + if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", jQuery.ready, false ); + + // If IE event model is used + } else if ( document.attachEvent ) { + // ensure firing before onload, + // maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", DOMContentLoaded ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", jQuery.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var toplevel = false; + + try { + toplevel = window.frameElement == null; + } catch(e) {} + + if ( document.documentElement.doScroll && toplevel ) { + doScrollCheck(); + } + } + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + isWindow: function( obj ) { + return obj != null && obj == obj.window; + }, + + isNumeric: function( obj ) { + return !isNaN( parseFloat(obj) ) && isFinite( obj ); + }, + + type: function( obj ) { + return obj == null ? + String( obj ) : + class2type[ toString.call(obj) ] || "object"; + }, + + isPlainObject: function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + try { + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call(obj, "constructor") && + !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + for ( var name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw new Error( msg ); + }, + + parseJSON: function( data ) { + if ( typeof data !== "string" || !data ) { + return null; + } + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + // Attempt to parse using the native JSON parser first + if ( window.JSON && window.JSON.parse ) { + return window.JSON.parse( data ); + } + + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test( data.replace( rvalidescape, "@" ) + .replace( rvalidtokens, "]" ) + .replace( rvalidbraces, "")) ) { + + return ( new Function( "return " + data ) )(); + + } + jQuery.error( "Invalid JSON: " + data ); + }, + + // Cross-browser xml parsing + parseXML: function( data ) { + if ( typeof data !== "string" || !data ) { + return null; + } + var xml, tmp; + try { + if ( window.DOMParser ) { // Standard + tmp = new DOMParser(); + xml = tmp.parseFromString( data , "text/xml" ); + } else { // IE + xml = new ActiveXObject( "Microsoft.XMLDOM" ); + xml.async = "false"; + xml.loadXML( data ); + } + } catch( e ) { + xml = undefined; + } + if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; + }, + + noop: function() {}, + + // Evaluates a script in a global context + // Workarounds based on findings by Jim Driscoll + // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context + globalEval: function( data ) { + if ( data && rnotwhite.test( data ) ) { + // We use execScript on Internet Explorer + // We use an anonymous function so that context is window + // rather than jQuery in Firefox + ( window.execScript || function( data ) { + window[ "eval" ].call( window, data ); + } )( data ); + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, + length = object.length, + isObj = length === undefined || jQuery.isFunction( object ); + + if ( args ) { + if ( isObj ) { + for ( name in object ) { + if ( callback.apply( object[ name ], args ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.apply( object[ i++ ], args ) === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isObj ) { + for ( name in object ) { + if ( callback.call( object[ name ], name, object[ name ] ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) { + break; + } + } + } + } + + return object; + }, + + // Use native String.trim function wherever possible + trim: trim ? + function( text ) { + return text == null ? + "" : + trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + text.toString().replace( trimLeft, "" ).replace( trimRight, "" ); + }, + + // results is for internal usage only + makeArray: function( array, results ) { + var ret = results || []; + + if ( array != null ) { + // The window, strings (and functions) also have 'length' + // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 + var type = jQuery.type( array ); + + if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { + push.call( ret, array ); + } else { + jQuery.merge( ret, array ); + } + } + + return ret; + }, + + inArray: function( elem, array, i ) { + var len; + + if ( array ) { + if ( indexOf ) { + return indexOf.call( array, elem, i ); + } + + len = array.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; + + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in array && array[ i ] === elem ) { + return i; + } + } + } + + return -1; + }, + + merge: function( first, second ) { + var i = first.length, + j = 0; + + if ( typeof second.length === "number" ) { + for ( var l = second.length; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, inv ) { + var ret = [], retVal; + inv = !!inv; + + // Go through the array, only saving the items + // that pass the validator function + for ( var i = 0, length = elems.length; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { + ret.push( elems[ i ] ); + } + } + + return ret; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var value, key, ret = [], + i = 0, + length = elems.length, + // jquery objects are treated as arrays + isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ; + + // Go through the array, translating each of the items to their + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + // Go through every key on the object, + } else { + for ( key in elems ) { + value = callback( elems[ key ], key, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + } + + // Flatten any nested arrays + return ret.concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + if ( typeof context === "string" ) { + var tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + var args = slice.call( arguments, 2 ), + proxy = function() { + return fn.apply( context, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; + + return proxy; + }, + + // Mutifunctional method to get and set values to a collection + // The value/s can optionally be executed if it's a function + access: function( elems, fn, key, value, chainable, emptyGet, pass ) { + var exec, + bulk = key == null, + i = 0, + length = elems.length; + + // Sets many values + if ( key && typeof key === "object" ) { + for ( i in key ) { + jQuery.access( elems, fn, i, key[i], 1, emptyGet, value ); + } + chainable = 1; + + // Sets one value + } else if ( value !== undefined ) { + // Optionally, function values get executed if exec is true + exec = pass === undefined && jQuery.isFunction( value ); + + if ( bulk ) { + // Bulk operations only iterate when executing function values + if ( exec ) { + exec = fn; + fn = function( elem, key, value ) { + return exec.call( jQuery( elem ), value ); + }; + + // Otherwise they run against the entire set + } else { + fn.call( elems, value ); + fn = null; + } + } + + if ( fn ) { + for (; i < length; i++ ) { + fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); + } + } + + chainable = 1; + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + length ? fn( elems[0], key ) : emptyGet; + }, + + now: function() { + return ( new Date() ).getTime(); + }, + + // Use of jQuery.browser is frowned upon. + // More details: http://docs.jquery.com/Utilities/jQuery.browser + uaMatch: function( ua ) { + ua = ua.toLowerCase(); + + var match = rwebkit.exec( ua ) || + ropera.exec( ua ) || + rmsie.exec( ua ) || + ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || + []; + + return { browser: match[1] || "", version: match[2] || "0" }; + }, + + sub: function() { + function jQuerySub( selector, context ) { + return new jQuerySub.fn.init( selector, context ); + } + jQuery.extend( true, jQuerySub, this ); + jQuerySub.superclass = this; + jQuerySub.fn = jQuerySub.prototype = this(); + jQuerySub.fn.constructor = jQuerySub; + jQuerySub.sub = this.sub; + jQuerySub.fn.init = function init( selector, context ) { + if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) { + context = jQuerySub( context ); + } + + return jQuery.fn.init.call( this, selector, context, rootjQuerySub ); + }; + jQuerySub.fn.init.prototype = jQuerySub.fn; + var rootjQuerySub = jQuerySub(document); + return jQuerySub; + }, + + browser: {} +}); + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +browserMatch = jQuery.uaMatch( userAgent ); +if ( browserMatch.browser ) { + jQuery.browser[ browserMatch.browser ] = true; + jQuery.browser.version = browserMatch.version; +} + +// Deprecated, use jQuery.browser.webkit instead +if ( jQuery.browser.webkit ) { + jQuery.browser.safari = true; +} + +// IE doesn't match non-breaking spaces with \s +if ( rnotwhite.test( "\xA0" ) ) { + trimLeft = /^[\s\xA0]+/; + trimRight = /[\s\xA0]+$/; +} + +// All jQuery objects should point back to these +rootjQuery = jQuery(document); + +// Cleanup functions for the document ready method +if ( document.addEventListener ) { + DOMContentLoaded = function() { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + jQuery.ready(); + }; + +} else if ( document.attachEvent ) { + DOMContentLoaded = function() { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( document.readyState === "complete" ) { + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + jQuery.ready(); + } + }; +} + +// The DOM ready check for Internet Explorer +function doScrollCheck() { + if ( jQuery.isReady ) { + return; + } + + try { + // If IE is used, use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + document.documentElement.doScroll("left"); + } catch(e) { + setTimeout( doScrollCheck, 1 ); + return; + } + + // and execute any waiting functions + jQuery.ready(); +} + +return jQuery; + +})(); + + +// String to Object flags format cache +var flagsCache = {}; + +// Convert String-formatted flags into Object-formatted ones and store in cache +function createFlags( flags ) { + var object = flagsCache[ flags ] = {}, + i, length; + flags = flags.split( /\s+/ ); + for ( i = 0, length = flags.length; i < length; i++ ) { + object[ flags[i] ] = true; + } + return object; +} + +/* + * Create a callback list using the following parameters: + * + * flags: an optional list of space-separated flags that will change how + * the callback list behaves + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible flags: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( flags ) { + + // Convert flags from String-formatted to Object-formatted + // (we check in cache first) + flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {}; + + var // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = [], + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // Flag to know if list is currently firing + firing, + // First callback to fire (used internally by add and fireWith) + firingStart, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // Add one or several callbacks to the list + add = function( args ) { + var i, + length, + elem, + type, + actual; + for ( i = 0, length = args.length; i < length; i++ ) { + elem = args[ i ]; + type = jQuery.type( elem ); + if ( type === "array" ) { + // Inspect recursively + add( elem ); + } else if ( type === "function" ) { + // Add if not in unique mode and callback is not in + if ( !flags.unique || !self.has( elem ) ) { + list.push( elem ); + } + } + } + }, + // Fire callbacks + fire = function( context, args ) { + args = args || []; + memory = !flags.memory || [ context, args ]; + fired = true; + firing = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) { + memory = true; // Mark as halted + break; + } + } + firing = false; + if ( list ) { + if ( !flags.once ) { + if ( stack && stack.length ) { + memory = stack.shift(); + self.fireWith( memory[ 0 ], memory[ 1 ] ); + } + } else if ( memory === true ) { + self.disable(); + } else { + list = []; + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + var length = list.length; + add( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away, unless previous + // firing was halted (stopOnFalse) + } else if ( memory && memory !== true ) { + firingStart = length; + fire( memory[ 0 ], memory[ 1 ] ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + var args = arguments, + argIndex = 0, + argLength = args.length; + for ( ; argIndex < argLength ; argIndex++ ) { + for ( var i = 0; i < list.length; i++ ) { + if ( args[ argIndex ] === list[ i ] ) { + // Handle firingIndex and firingLength + if ( firing ) { + if ( i <= firingLength ) { + firingLength--; + if ( i <= firingIndex ) { + firingIndex--; + } + } + } + // Remove the element + list.splice( i--, 1 ); + // If we have some unicity property then + // we only need to do this once + if ( flags.unique ) { + break; + } + } + } + } + } + return this; + }, + // Control if a given callback is in the list + has: function( fn ) { + if ( list ) { + var i = 0, + length = list.length; + for ( ; i < length; i++ ) { + if ( fn === list[ i ] ) { + return true; + } + } + } + return false; + }, + // Remove all callbacks from the list + empty: function() { + list = []; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory || memory === true ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( stack ) { + if ( firing ) { + if ( !flags.once ) { + stack.push( [ context, args ] ); + } + } else if ( !( flags.once && memory ) ) { + fire( context, args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + + + +var // Static reference to slice + sliceDeferred = [].slice; + +jQuery.extend({ + + Deferred: function( func ) { + var doneList = jQuery.Callbacks( "once memory" ), + failList = jQuery.Callbacks( "once memory" ), + progressList = jQuery.Callbacks( "memory" ), + state = "pending", + lists = { + resolve: doneList, + reject: failList, + notify: progressList + }, + promise = { + done: doneList.add, + fail: failList.add, + progress: progressList.add, + + state: function() { + return state; + }, + + // Deprecated + isResolved: doneList.fired, + isRejected: failList.fired, + + then: function( doneCallbacks, failCallbacks, progressCallbacks ) { + deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks ); + return this; + }, + always: function() { + deferred.done.apply( deferred, arguments ).fail.apply( deferred, arguments ); + return this; + }, + pipe: function( fnDone, fnFail, fnProgress ) { + return jQuery.Deferred(function( newDefer ) { + jQuery.each( { + done: [ fnDone, "resolve" ], + fail: [ fnFail, "reject" ], + progress: [ fnProgress, "notify" ] + }, function( handler, data ) { + var fn = data[ 0 ], + action = data[ 1 ], + returned; + if ( jQuery.isFunction( fn ) ) { + deferred[ handler ](function() { + returned = fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); + } + }); + } else { + deferred[ handler ]( newDefer[ action ] ); + } + }); + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + if ( obj == null ) { + obj = promise; + } else { + for ( var key in promise ) { + obj[ key ] = promise[ key ]; + } + } + return obj; + } + }, + deferred = promise.promise({}), + key; + + for ( key in lists ) { + deferred[ key ] = lists[ key ].fire; + deferred[ key + "With" ] = lists[ key ].fireWith; + } + + // Handle state + deferred.done( function() { + state = "resolved"; + }, failList.disable, progressList.lock ).fail( function() { + state = "rejected"; + }, doneList.disable, progressList.lock ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( firstParam ) { + var args = sliceDeferred.call( arguments, 0 ), + i = 0, + length = args.length, + pValues = new Array( length ), + count = length, + pCount = length, + deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ? + firstParam : + jQuery.Deferred(), + promise = deferred.promise(); + function resolveFunc( i ) { + return function( value ) { + args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + if ( !( --count ) ) { + deferred.resolveWith( deferred, args ); + } + }; + } + function progressFunc( i ) { + return function( value ) { + pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + deferred.notifyWith( promise, pValues ); + }; + } + if ( length > 1 ) { + for ( ; i < length; i++ ) { + if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) { + args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) ); + } else { + --count; + } + } + if ( !count ) { + deferred.resolveWith( deferred, args ); + } + } else if ( deferred !== firstParam ) { + deferred.resolveWith( deferred, length ? [ firstParam ] : [] ); + } + return promise; + } +}); + + + + +jQuery.support = (function() { + + var support, + all, + a, + select, + opt, + input, + fragment, + tds, + events, + eventName, + i, + isSupported, + div = document.createElement( "div" ), + documentElement = document.documentElement; + + // Preliminary tests + div.setAttribute("className", "t"); + div.innerHTML = "
a"; + + all = div.getElementsByTagName( "*" ); + a = div.getElementsByTagName( "a" )[ 0 ]; + + // Can't get basic test support + if ( !all || !all.length || !a ) { + return {}; + } + + // First batch of supports tests + select = document.createElement( "select" ); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName( "input" )[ 0 ]; + + support = { + // IE strips leading whitespace when .innerHTML is used + leadingWhitespace: ( div.firstChild.nodeType === 3 ), + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + tbody: !div.getElementsByTagName("tbody").length, + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + htmlSerialize: !!div.getElementsByTagName("link").length, + + // Get the style information from getAttribute + // (IE uses .cssText instead) + style: /top/.test( a.getAttribute("style") ), + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + hrefNormalized: ( a.getAttribute("href") === "/a" ), + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + opacity: /^0.55/.test( a.style.opacity ), + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + cssFloat: !!a.style.cssFloat, + + // Make sure that if no value is specified for a checkbox + // that it defaults to "on". + // (WebKit defaults to "" instead) + checkOn: ( input.value === "on" ), + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + optSelected: opt.selected, + + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + getSetAttribute: div.className !== "t", + + // Tests for enctype support on a form(#6743) + enctype: !!document.createElement("form").enctype, + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", + + // Will be defined later + submitBubbles: true, + changeBubbles: true, + focusinBubbles: false, + deleteExpando: true, + noCloneEvent: true, + inlineBlockNeedsLayout: false, + shrinkWrapBlocks: false, + reliableMarginRight: true, + pixelMargin: true + }; + + // jQuery.boxModel DEPRECATED in 1.3, use jQuery.support.boxModel instead + jQuery.boxModel = support.boxModel = (document.compatMode === "CSS1Compat"); + + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as disabled) + select.disabled = true; + support.optDisabled = !opt.disabled; + + // Test to see if it's possible to delete an expando from an element + // Fails in Internet Explorer + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + + if ( !div.addEventListener && div.attachEvent && div.fireEvent ) { + div.attachEvent( "onclick", function() { + // Cloning a node shouldn't copy over any + // bound event handlers (IE does this) + support.noCloneEvent = false; + }); + div.cloneNode( true ).fireEvent( "onclick" ); + } + + // Check if a radio maintains its value + // after being appended to the DOM + input = document.createElement("input"); + input.value = "t"; + input.setAttribute("type", "radio"); + support.radioValue = input.value === "t"; + + input.setAttribute("checked", "checked"); + + // #11217 - WebKit loses check when the name is after the checked attribute + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + fragment = document.createDocumentFragment(); + fragment.appendChild( div.lastChild ); + + // WebKit doesn't clone checked state correctly in fragments + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; + + fragment.removeChild( input ); + fragment.appendChild( div ); + + // Technique from Juriy Zaytsev + // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/ + // We only care about the case where non-standard event systems + // are used, namely in IE. Short-circuiting here helps us to + // avoid an eval call (in setAttribute) which can cause CSP + // to go haywire. See: https://developer.mozilla.org/en/Security/CSP + if ( div.attachEvent ) { + for ( i in { + submit: 1, + change: 1, + focusin: 1 + }) { + eventName = "on" + i; + isSupported = ( eventName in div ); + if ( !isSupported ) { + div.setAttribute( eventName, "return;" ); + isSupported = ( typeof div[ eventName ] === "function" ); + } + support[ i + "Bubbles" ] = isSupported; + } + } + + fragment.removeChild( div ); + + // Null elements to avoid leaks in IE + fragment = select = opt = div = input = null; + + // Run tests that need a body at doc ready + jQuery(function() { + var container, outer, inner, table, td, offsetSupport, + marginDiv, conMarginTop, style, html, positionTopLeftWidthHeight, + paddingMarginBorderVisibility, paddingMarginBorder, + body = document.getElementsByTagName("body")[0]; + + if ( !body ) { + // Return for frameset docs that don't have a body + return; + } + + conMarginTop = 1; + paddingMarginBorder = "padding:0;margin:0;border:"; + positionTopLeftWidthHeight = "position:absolute;top:0;left:0;width:1px;height:1px;"; + paddingMarginBorderVisibility = paddingMarginBorder + "0;visibility:hidden;"; + style = "style='" + positionTopLeftWidthHeight + paddingMarginBorder + "5px solid #000;"; + html = "
" + + "" + + "
"; + + container = document.createElement("div"); + container.style.cssText = paddingMarginBorderVisibility + "width:0;height:0;position:static;top:0;margin-top:" + conMarginTop + "px"; + body.insertBefore( container, body.firstChild ); + + // Construct the test element + div = document.createElement("div"); + container.appendChild( div ); + + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + // (only IE 8 fails this test) + div.innerHTML = "
t
"; + tds = div.getElementsByTagName( "td" ); + isSupported = ( tds[ 0 ].offsetHeight === 0 ); + + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; + + // Check if empty table cells still have offsetWidth/Height + // (IE <= 8 fail this test) + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); + + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. For more + // info see bug #3333 + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + if ( window.getComputedStyle ) { + div.innerHTML = ""; + marginDiv = document.createElement( "div" ); + marginDiv.style.width = "0"; + marginDiv.style.marginRight = "0"; + div.style.width = "2px"; + div.appendChild( marginDiv ); + support.reliableMarginRight = + ( parseInt( ( window.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0; + } + + if ( typeof div.style.zoom !== "undefined" ) { + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + // (IE < 8 does this) + div.innerHTML = ""; + div.style.width = div.style.padding = "1px"; + div.style.border = 0; + div.style.overflow = "hidden"; + div.style.display = "inline"; + div.style.zoom = 1; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); + + // Check if elements with layout shrink-wrap their children + // (IE 6 does this) + div.style.display = "block"; + div.style.overflow = "visible"; + div.innerHTML = "
"; + support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); + } + + div.style.cssText = positionTopLeftWidthHeight + paddingMarginBorderVisibility; + div.innerHTML = html; + + outer = div.firstChild; + inner = outer.firstChild; + td = outer.nextSibling.firstChild.firstChild; + + offsetSupport = { + doesNotAddBorder: ( inner.offsetTop !== 5 ), + doesAddBorderForTableAndCells: ( td.offsetTop === 5 ) + }; + + inner.style.position = "fixed"; + inner.style.top = "20px"; + + // safari subtracts parent border width here which is 5px + offsetSupport.fixedPosition = ( inner.offsetTop === 20 || inner.offsetTop === 15 ); + inner.style.position = inner.style.top = ""; + + outer.style.overflow = "hidden"; + outer.style.position = "relative"; + + offsetSupport.subtractsBorderForOverflowNotVisible = ( inner.offsetTop === -5 ); + offsetSupport.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== conMarginTop ); + + if ( window.getComputedStyle ) { + div.style.marginTop = "1%"; + support.pixelMargin = ( window.getComputedStyle( div, null ) || { marginTop: 0 } ).marginTop !== "1%"; + } + + if ( typeof container.style.zoom !== "undefined" ) { + container.style.zoom = 1; + } + + body.removeChild( container ); + marginDiv = div = container = null; + + jQuery.extend( support, offsetSupport ); + }); + + return support; +})(); + + + + +var rbrace = /^(?:\{.*\}|\[.*\])$/, + rmultiDash = /([A-Z])/g; + +jQuery.extend({ + cache: {}, + + // Please use with caution + uuid: 0, + + // Unique for each copy of jQuery on the page + // Non-digits removed to match rinlinejQuery + expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", + "applet": true + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + + data: function( elem, name, data, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var privateCache, thisCache, ret, + internalKey = jQuery.expando, + getByName = typeof name === "string", + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey, + isEvents = name === "events"; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + elem[ internalKey ] = id = ++jQuery.uuid; + } else { + id = internalKey; + } + } + + if ( !cache[ id ] ) { + cache[ id ] = {}; + + // Avoids exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + if ( !isNode ) { + cache[ id ].toJSON = jQuery.noop; + } + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } + } + + privateCache = thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } + + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Users should not attempt to inspect the internal events object using jQuery.data, + // it is undocumented and subject to change. But does anyone listen? No. + if ( isEvents && !thisCache[ name ] ) { + return privateCache.events; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( getByName ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; + }, + + removeData: function( elem, name, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, i, l, + + // Reference to internal data cache key + internalKey = jQuery.expando, + + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + + // See jQuery.data for more information + id = isNode ? elem[ internalKey ] : internalKey; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + + if ( thisCache ) { + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split( " " ); + } + } + } + + for ( i = 0, l = name.length; i < l; i++ ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject(cache[ id ]) ) { + return; + } + } + + // Browsers that fail expando deletion also refuse to delete expandos on + // the window, but it will allow it on all other JS objects; other browsers + // don't care + // Ensure that `cache` is not a window object #10080 + if ( jQuery.support.deleteExpando || !cache.setInterval ) { + delete cache[ id ]; + } else { + cache[ id ] = null; + } + + // We destroyed the cache and need to eliminate the expando on the node to avoid + // false lookups in the cache for entries that no longer exist + if ( isNode ) { + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( jQuery.support.deleteExpando ) { + delete elem[ internalKey ]; + } else if ( elem.removeAttribute ) { + elem.removeAttribute( internalKey ); + } else { + elem[ internalKey ] = null; + } + } + }, + + // For internal use only. + _data: function( elem, name, data ) { + return jQuery.data( elem, name, data, true ); + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + if ( elem.nodeName ) { + var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; + + if ( match ) { + return !(match === true || elem.getAttribute("classid") !== match); + } + } + + return true; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var parts, part, attr, name, l, + elem = this[0], + i = 0, + data = null; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = jQuery.data( elem ); + + if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { + attr = elem.attributes; + for ( l = attr.length; i < l; i++ ) { + name = attr[i].name; + + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.substring(5) ); + + dataAttr( elem, name, data[ name ] ); + } + } + jQuery._data( elem, "parsedAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + parts = key.split( ".", 2 ); + parts[1] = parts[1] ? "." + parts[1] : ""; + part = parts[1] + "!"; + + return jQuery.access( this, function( value ) { + + if ( value === undefined ) { + data = this.triggerHandler( "getData" + part, [ parts[0] ] ); + + // Try to fetch any internally stored data first + if ( data === undefined && elem ) { + data = jQuery.data( elem, key ); + data = dataAttr( elem, key, data ); + } + + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + } + + parts[1] = value; + this.each(function() { + var self = jQuery( this ); + + self.triggerHandler( "setData" + part, parts ); + jQuery.data( this, key, value ); + self.triggerHandler( "changeData" + part, parts ); + }); + }, null, value, arguments.length > 1, null, false ); + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + jQuery.isNumeric( data ) ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + for ( var name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} + + + + +function handleQueueMarkDefer( elem, type, src ) { + var deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + defer = jQuery._data( elem, deferDataKey ); + if ( defer && + ( src === "queue" || !jQuery._data(elem, queueDataKey) ) && + ( src === "mark" || !jQuery._data(elem, markDataKey) ) ) { + // Give room for hard-coded callbacks to fire first + // and eventually mark/queue something else on the element + setTimeout( function() { + if ( !jQuery._data( elem, queueDataKey ) && + !jQuery._data( elem, markDataKey ) ) { + jQuery.removeData( elem, deferDataKey, true ); + defer.fire(); + } + }, 0 ); + } +} + +jQuery.extend({ + + _mark: function( elem, type ) { + if ( elem ) { + type = ( type || "fx" ) + "mark"; + jQuery._data( elem, type, (jQuery._data( elem, type ) || 0) + 1 ); + } + }, + + _unmark: function( force, elem, type ) { + if ( force !== true ) { + type = elem; + elem = force; + force = false; + } + if ( elem ) { + type = type || "fx"; + var key = type + "mark", + count = force ? 0 : ( (jQuery._data( elem, key ) || 1) - 1 ); + if ( count ) { + jQuery._data( elem, key, count ); + } else { + jQuery.removeData( elem, key, true ); + handleQueueMarkDefer( elem, type, "mark" ); + } + } + }, + + queue: function( elem, type, data ) { + var q; + if ( elem ) { + type = ( type || "fx" ) + "queue"; + q = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !q || jQuery.isArray(data) ) { + q = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + q.push( data ); + } + } + return q || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + fn = queue.shift(), + hooks = {}; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + } + + if ( fn ) { + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + jQuery._data( elem, type + ".run", hooks ); + fn.call( elem, function() { + jQuery.dequeue( elem, type ); + }, hooks ); + } + + if ( !queue.length ) { + jQuery.removeData( elem, type + "queue " + type + ".run", true ); + handleQueueMarkDefer( elem, type, "queue" ); + } + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[0], type ); + } + + return data === undefined ? + this : + this.each(function() { + var queue = jQuery.queue( this, type, data ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, object ) { + if ( typeof type !== "string" ) { + object = type; + type = undefined; + } + type = type || "fx"; + var defer = jQuery.Deferred(), + elements = this, + i = elements.length, + count = 1, + deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + tmp; + function resolve() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + } + while( i-- ) { + if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) || + ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) || + jQuery.data( elements[ i ], markDataKey, undefined, true ) ) && + jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) { + count++; + tmp.add( resolve ); + } + } + resolve(); + return defer.promise( object ); + } +}); + + + + +var rclass = /[\n\t\r]/g, + rspace = /\s+/, + rreturn = /\r/g, + rtype = /^(?:button|input)$/i, + rfocusable = /^(?:button|input|object|select|textarea)$/i, + rclickable = /^a(?:rea)?$/i, + rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute, + nodeHook, boolHook, fixSpecified; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, + + prop: function( name, value ) { + return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} + }); + }, + + addClass: function( value ) { + var classNames, i, l, elem, + setClass, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call(this, j, this.className) ); + }); + } + + if ( value && typeof value === "string" ) { + classNames = value.split( rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + + if ( elem.nodeType === 1 ) { + if ( !elem.className && classNames.length === 1 ) { + elem.className = value; + + } else { + setClass = " " + elem.className + " "; + + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + if ( !~setClass.indexOf( " " + classNames[ c ] + " " ) ) { + setClass += classNames[ c ] + " "; + } + } + elem.className = jQuery.trim( setClass ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classNames, i, l, elem, className, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call(this, j, this.className) ); + }); + } + + if ( (value && typeof value === "string") || value === undefined ) { + classNames = ( value || "" ).split( rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + + if ( elem.nodeType === 1 && elem.className ) { + if ( value ) { + className = (" " + elem.className + " ").replace( rclass, " " ); + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + className = className.replace(" " + classNames[ c ] + " ", " "); + } + elem.className = jQuery.trim( className ); + + } else { + elem.className = ""; + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isBool = typeof stateVal === "boolean"; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + state = stateVal, + classNames = value.split( rspace ); + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space seperated list + state = isBool ? state : !self.hasClass( className ); + self[ state ? "addClass" : "removeClass" ]( className ); + } + + } else if ( type === "undefined" || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery._data( this, "__className__", this.className ); + } + + // toggle whole className + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + var hooks, ret, isFunction, + elem = this[0]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; + } + + ret = elem.value; + + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; + } + + return; + } + + isFunction = jQuery.isFunction( value ); + + return this.each(function( i ) { + var self = jQuery(this), val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call( this, i, self.val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { + return value == null ? "" : value + ""; + }); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + }, + select: { + get: function( elem ) { + var value, i, max, option, + index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type === "select-one"; + + // Nothing was selected + if ( index < 0 ) { + return null; + } + + // Loop through all the selected options + i = one ? index : 0; + max = one ? index + 1 : options.length; + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Don't return options that are disabled or in a disabled optgroup + if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && + (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + // Fixes Bug #2551 -- select.val() broken in IE after form.reset() + if ( one && !values.length && options.length ) { + return jQuery( options[ index ] ).val(); + } + + return values; + }, + + set: function( elem, value ) { + var values = jQuery.makeArray( value ); + + jQuery(elem).find("option").each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + elem.selectedIndex = -1; + } + return values; + } + } + }, + + attrFn: { + val: true, + css: true, + html: true, + text: true, + data: true, + width: true, + height: true, + offset: true + }, + + attr: function( elem, name, value, pass ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( pass && name in jQuery.attrFn ) { + return jQuery( elem )[ name ]( value ); + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( notxml ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); + } + + if ( value !== undefined ) { + + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + + } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + elem.setAttribute( name, "" + value ); + return value; + } + + } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + + ret = elem.getAttribute( name ); + + // Non-existent attributes return null, we normalize to undefined + return ret === null ? + undefined : + ret; + } + }, + + removeAttr: function( elem, value ) { + var propName, attrNames, name, l, isBool, + i = 0; + + if ( value && elem.nodeType === 1 ) { + attrNames = value.toLowerCase().split( rspace ); + l = attrNames.length; + + for ( ; i < l; i++ ) { + name = attrNames[ i ]; + + if ( name ) { + propName = jQuery.propFix[ name ] || name; + isBool = rboolean.test( name ); + + // See #9699 for explanation of this approach (setting first, then removal) + // Do not do this for boolean attributes (see #10870) + if ( !isBool ) { + jQuery.attr( elem, name, "" ); + } + elem.removeAttribute( getSetAttribute ? name : propName ); + + // Set corresponding property to false for boolean attributes + if ( isBool && propName in elem ) { + elem[ propName ] = false; + } + } + } + } + }, + + attrHooks: { + type: { + set: function( elem, value ) { + // We can't allow the type property to be changed (since it causes problems in IE) + if ( rtype.test( elem.nodeName ) && elem.parentNode ) { + jQuery.error( "type property can't be changed" ); + } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to it's default in case type is set after value + // This is for element creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + }, + // Use the value property for back compat + // Use the nodeHook for button elements in IE6/7 (#1954) + value: { + get: function( elem, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.get( elem, name ); + } + return name in elem ? + elem.value : + null; + }, + set: function( elem, value, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.set( elem, value, name ); + } + // Does not return so that setAttribute is also used + elem.value = value; + } + } + }, + + propFix: { + tabindex: "tabIndex", + readonly: "readOnly", + "for": "htmlFor", + "class": "className", + maxlength: "maxLength", + cellspacing: "cellSpacing", + cellpadding: "cellPadding", + rowspan: "rowSpan", + colspan: "colSpan", + usemap: "useMap", + frameborder: "frameBorder", + contenteditable: "contentEditable" + }, + + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + return ( elem[ name ] = value ); + } + + } else { + if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + return elem[ name ]; + } + } + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + var attributeNode = elem.getAttributeNode("tabindex"); + + return attributeNode && attributeNode.specified ? + parseInt( attributeNode.value, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + } + } +}); + +// Add the tabIndex propHook to attrHooks for back-compat (different case is intentional) +jQuery.attrHooks.tabindex = jQuery.propHooks.tabIndex; + +// Hook for boolean attributes +boolHook = { + get: function( elem, name ) { + // Align boolean attributes with corresponding properties + // Fall back to attribute presence where some booleans are not supported + var attrNode, + property = jQuery.prop( elem, name ); + return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ? + name.toLowerCase() : + undefined; + }, + set: function( elem, value, name ) { + var propName; + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + // value is true since we know at this point it's type boolean and not false + // Set boolean attributes to the same name and set the DOM property + propName = jQuery.propFix[ name ] || name; + if ( propName in elem ) { + // Only set the IDL specifically if it already exists on the element + elem[ propName ] = true; + } + + elem.setAttribute( name, name.toLowerCase() ); + } + return name; + } +}; + +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { + + fixSpecified = { + name: true, + id: true, + coords: true + }; + + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = jQuery.valHooks.button = { + get: function( elem, name ) { + var ret; + ret = elem.getAttributeNode( name ); + return ret && ( fixSpecified[ name ] ? ret.nodeValue !== "" : ret.specified ) ? + ret.nodeValue : + undefined; + }, + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + ret = document.createAttribute( name ); + elem.setAttributeNode( ret ); + } + return ( ret.nodeValue = value + "" ); + } + }; + + // Apply the nodeHook to tabindex + jQuery.attrHooks.tabindex.set = nodeHook.set; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; + } + } + }); + }); + + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + get: nodeHook.get, + set: function( elem, value, name ) { + if ( value === "" ) { + value = "false"; + } + nodeHook.set( elem, value, name ); + } + }; +} + + +// Some attributes require a special call on IE +if ( !jQuery.support.hrefNormalized ) { + jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + get: function( elem ) { + var ret = elem.getAttribute( name, 2 ); + return ret === null ? undefined : ret; + } + }); + }); +} + +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Normalize to lowercase since IE uppercases css property names + return elem.style.cssText.toLowerCase() || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = "" + value ); + } + }; +} + +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { + get: function( elem ) { + var parent = elem.parentNode; + + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + return null; + } + }); +} + +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + +// Radios and checkboxes getter/setter +if ( !jQuery.support.checkOn ) { + jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + get: function( elem ) { + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + } + }; + }); +} +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } + } + }); +}); + + + + +var rformElems = /^(?:textarea|input|select)$/i, + rtypenamespace = /^([^\.]*)?(?:\.(.+))?$/, + rhoverHack = /(?:^|\s)hover(\.\S+)?\b/, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rquickIs = /^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/, + quickParse = function( selector ) { + var quick = rquickIs.exec( selector ); + if ( quick ) { + // 0 1 2 3 + // [ _, tag, id, class ] + quick[1] = ( quick[1] || "" ).toLowerCase(); + quick[3] = quick[3] && new RegExp( "(?:^|\\s)" + quick[3] + "(?:\\s|$)" ); + } + return quick; + }, + quickIs = function( elem, m ) { + var attrs = elem.attributes || {}; + return ( + (!m[1] || elem.nodeName.toLowerCase() === m[1]) && + (!m[2] || (attrs.id || {}).value === m[2]) && + (!m[3] || m[3].test( (attrs[ "class" ] || {}).value )) + ); + }, + hoverHack = function( events ) { + return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" ); + }; + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + add: function( elem, types, handler, data, selector ) { + + var elemData, eventHandle, events, + t, tns, type, namespaces, handleObj, + handleObjIn, quick, handlers, special; + + // Don't attach events to noData or text/comment nodes (allow plain objects tho) + if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + events = elemData.events; + if ( !events ) { + elemData.events = events = {}; + } + eventHandle = elemData.handle; + if ( !eventHandle ) { + elemData.handle = eventHandle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } + + // Handle multiple events separated by a space + // jQuery(...).bind("mouseover mouseout", fn); + types = jQuery.trim( hoverHack(types) ).split( " " ); + for ( t = 0; t < types.length; t++ ) { + + tns = rtypenamespace.exec( types[t] ) || []; + type = tns[1]; + namespaces = ( tns[2] || "" ).split( "." ).sort(); + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: tns[1], + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + quick: selector && quickParse( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + handlers = events[ type ]; + if ( !handlers ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener/attachEvent if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + global: {}, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var elemData = jQuery.hasData( elem ) && jQuery._data( elem ), + t, tns, type, origType, namespaces, origCount, + j, events, special, handle, eventType, handleObj; + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = jQuery.trim( hoverHack( types || "" ) ).split(" "); + for ( t = 0; t < types.length; t++ ) { + tns = rtypenamespace.exec( types[t] ) || []; + type = origType = tns[1]; + namespaces = tns[2]; + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector? special.delegateType : special.bindType ) || type; + eventType = events[ type ] || []; + origCount = eventType.length; + namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.)?") + "(\\.|$)") : null; + + // Remove matching events + for ( j = 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !namespaces || namespaces.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + eventType.splice( j--, 1 ); + + if ( handleObj.selector ) { + eventType.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( eventType.length === 0 && origCount !== eventType.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + handle = elemData.handle; + if ( handle ) { + handle.elem = null; + } + + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery.removeData( elem, [ "events", "handle" ], true ); + } + }, + + // Events that are safe to short-circuit if no handlers are attached. + // Native DOM events should not be added, they may have inline handlers. + customEvent: { + "getData": true, + "setData": true, + "changeData": true + }, + + trigger: function( event, data, elem, onlyHandlers ) { + // Don't do events on text and comment nodes + if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) { + return; + } + + // Event object or event type + var type = event.type || event, + namespaces = [], + cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType; + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "!" ) >= 0 ) { + // Exclusive events trigger only for the exact event (no namespaces) + type = type.slice(0, -1); + exclusive = true; + } + + if ( type.indexOf( "." ) >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + + if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { + // No jQuery handlers for this event type, and it can't have inline handlers + return; + } + + // Caller can pass in an Event, Object, or just an event type string + event = typeof event === "object" ? + // jQuery.Event object + event[ jQuery.expando ] ? event : + // Object literal + new jQuery.Event( type, event ) : + // Just the event type (string) + new jQuery.Event( type ); + + event.type = type; + event.isTrigger = true; + event.exclusive = exclusive; + event.namespace = namespaces.join( "." ); + event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") : null; + ontype = type.indexOf( ":" ) < 0 ? "on" + type : ""; + + // Handle a global trigger + if ( !elem ) { + + // TODO: Stop taunting the data cache; remove global events and always attach to document + cache = jQuery.cache; + for ( i in cache ) { + if ( cache[ i ].events && cache[ i ].events[ type ] ) { + jQuery.event.trigger( event, data, cache[ i ].handle.elem, true ); + } + } + return; + } + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data != null ? jQuery.makeArray( data ) : []; + data.unshift( event ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + eventPath = [[ elem, special.bindType || type ]]; + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode; + old = null; + for ( ; cur; cur = cur.parentNode ) { + eventPath.push([ cur, bubbleType ]); + old = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( old && old === elem.ownerDocument ) { + eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]); + } + } + + // Fire handlers on the event path + for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) { + + cur = eventPath[i][0]; + event.type = eventPath[i][1]; + + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + // Note that this is a bare JS function and not a jQuery handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply( cur, data ) === false ) { + event.preventDefault(); + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && + !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + // IE<9 dies on focus/blur to hidden element (#1486) + if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + old = elem[ ontype ]; + + if ( old ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + elem[ type ](); + jQuery.event.triggered = undefined; + + if ( old ) { + elem[ ontype ] = old; + } + } + } + } + + return event.result; + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event || window.event ); + + var handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), + delegateCount = handlers.delegateCount, + args = [].slice.call( arguments, 0 ), + run_all = !event.exclusive && !event.namespace, + special = jQuery.event.special[ event.type ] || {}, + handlerQueue = [], + i, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers that should run if there are delegated events + // Avoid non-left-click bubbling in Firefox (#3861) + if ( delegateCount && !(event.button && event.type === "click") ) { + + // Pregenerate a single jQuery object for reuse with .is() + jqcur = jQuery(this); + jqcur.context = this.ownerDocument || this; + + for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { + + // Don't process events on disabled elements (#6911, #8165) + if ( cur.disabled !== true ) { + selMatch = {}; + matches = []; + jqcur[0] = cur; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + sel = handleObj.selector; + + if ( selMatch[ sel ] === undefined ) { + selMatch[ sel ] = ( + handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel ) + ); + } + if ( selMatch[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, matches: matches }); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( handlers.length > delegateCount ) { + handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); + } + + // Run delegates first; they may want to stop propagation beneath us + for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { + matched = handlerQueue[ i ]; + event.currentTarget = matched.elem; + + for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { + handleObj = matched.matches[ j ]; + + // Triggered event must either 1) be non-exclusive and have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { + + event.data = handleObj.data; + event.handleObj = handleObj; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + // *** attrChange attrName relatedNode srcElement are not normalized, non-W3C, deprecated, will be removed in 1.8 *** + props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, + originalEvent = event, + fixHook = jQuery.event.fixHooks[ event.type ] || {}, + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = jQuery.Event( originalEvent ); + + for ( i = copy.length; i; ) { + prop = copy[ --i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2) + if ( !event.target ) { + event.target = originalEvent.srcElement || document; + } + + // Target should not be a text node (#504, Safari) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // For mouse/key events; add metaKey if it's not there (#3368, IE6/7/8) + if ( event.metaKey === undefined ) { + event.metaKey = event.ctrlKey; + } + + return fixHook.filter? fixHook.filter( event, originalEvent ) : event; + }, + + special: { + ready: { + // Make sure the ready event is setup + setup: jQuery.bindReady + }, + + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + + focus: { + delegateType: "focusin" + }, + blur: { + delegateType: "focusout" + }, + + beforeunload: { + setup: function( data, namespaces, eventHandle ) { + // We only want to do this special case on windows + if ( jQuery.isWindow( this ) ) { + this.onbeforeunload = eventHandle; + } + }, + + teardown: function( namespaces, eventHandle ) { + if ( this.onbeforeunload === eventHandle ) { + this.onbeforeunload = null; + } + } + } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +// Some plugins are using, but it's undocumented/deprecated and will be removed. +// The 1.7 special event interface should provide all the hooks needed now. +jQuery.event.handle = jQuery.event.dispatch; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + if ( elem.detachEvent ) { + elem.detachEvent( "on" + type, handle ); + } + }; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +function returnFalse() { + return false; +} +function returnTrue() { + return true; +} + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + preventDefault: function() { + this.isDefaultPrevented = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + + // if preventDefault exists run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // otherwise set the returnValue property of the original event to false (IE) + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + this.isPropagationStopped = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + // if stopPropagation exists run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + // otherwise set the cancelBubble property of the original event to true (IE) + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + }, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var target = this, + related = event.relatedTarget, + handleObj = event.handleObj, + selector = handleObj.selector, + ret; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +}); + +// IE submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !form._submit_attached ) { + jQuery.event.add( form, "submit._submit", function( event ) { + event._submit_bubble = true; + }); + form._submit_attached = true; + } + }); + // return undefined since we don't need an event listener + }, + + postDispatch: function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( event._submit_bubble ) { + delete event._submit_bubble; + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + } + }, + + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); + } + }; +} + +// IE change delegation and checkbox/radio fix +if ( !jQuery.support.changeBubbles ) { + + jQuery.event.special.change = { + + setup: function() { + + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + jQuery.event.simulate( "change", this, event, true ); + } + }); + } + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; + + if ( rformElems.test( elem.nodeName ) && !elem._change_attached ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + elem._change_attached = true; + } + }); + }, + + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return rformElems.test( this.nodeName ); + } + }; +} + +// Create "bubbling" focus and blur events +if ( !jQuery.support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + if ( attaches++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --attaches === 0 ) { + document.removeEventListener( orig, handler, true ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { // && selector != null + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } + + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on( types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + var handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( var type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + bind: function( types, data, fn ) { + return this.on( types, null, data, fn ); + }, + unbind: function( types, fn ) { + return this.off( types, null, fn ); + }, + + live: function( types, data, fn ) { + jQuery( this.context ).on( types, this.selector, data, fn ); + return this; + }, + die: function( types, fn ) { + jQuery( this.context ).off( types, this.selector || "**", fn ); + return this; + }, + + delegate: function( selector, types, data, fn ) { + return this.on( types, selector, data, fn ); + }, + undelegate: function( selector, types, fn ) { + // ( namespace ) or ( selector, types [, fn] ) + return arguments.length == 1? this.off( selector, "**" ) : this.off( types, selector, fn ); + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + if ( this[0] ) { + return jQuery.event.trigger( type, data, this[0], true ); + } + }, + + toggle: function( fn ) { + // Save reference to arguments for access in closure + var args = arguments, + guid = fn.guid || jQuery.guid++, + i = 0, + toggler = function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + }; + + // link all the functions, so any of them can unbind this click handler + toggler.guid = guid; + while ( i < args.length ) { + args[ i++ ].guid = guid; + } + + return this.click( toggler ); + }, + + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + } +}); + +jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + if ( fn == null ) { + fn = data; + data = null; + } + + return arguments.length > 0 ? + this.on( name, null, data, fn ) : + this.trigger( name ); + }; + + if ( jQuery.attrFn ) { + jQuery.attrFn[ name ] = true; + } + + if ( rkeyEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks; + } + + if ( rmouseEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks; + } +}); + + + +/*! + * Sizzle CSS Selector Engine + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){ + +var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + expando = "sizcache" + (Math.random() + '').replace('.', ''), + done = 0, + toString = Object.prototype.toString, + hasDuplicate = false, + baseHasDuplicate = true, + rBackslash = /\\/g, + rReturn = /\r\n/g, + rNonWord = /\W/; + +// Here we check if the JavaScript engine is using some sort of +// optimization where it does not always call our comparision +// function. If that is the case, discard the hasDuplicate value. +// Thus far that includes Google Chrome. +[0, 0].sort(function() { + baseHasDuplicate = false; + return 0; +}); + +var Sizzle = function( selector, context, results, seed ) { + results = results || []; + context = context || document; + + var origContext = context; + + if ( context.nodeType !== 1 && context.nodeType !== 9 ) { + return []; + } + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + var m, set, checkSet, extra, ret, cur, pop, i, + prune = true, + contextXML = Sizzle.isXML( context ), + parts = [], + soFar = selector; + + // Reset the position of the chunker regexp (start from head) + do { + chunker.exec( "" ); + m = chunker.exec( soFar ); + + if ( m ) { + soFar = m[3]; + + parts.push( m[1] ); + + if ( m[2] ) { + extra = m[3]; + break; + } + } + } while ( m ); + + if ( parts.length > 1 && origPOS.exec( selector ) ) { + + if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { + set = posProcess( parts[0] + parts[1], context, seed ); + + } else { + set = Expr.relative[ parts[0] ] ? + [ context ] : + Sizzle( parts.shift(), context ); + + while ( parts.length ) { + selector = parts.shift(); + + if ( Expr.relative[ selector ] ) { + selector += parts.shift(); + } + + set = posProcess( selector, set, seed ); + } + } + + } else { + // Take a shortcut and set the context if the root selector is an ID + // (but not if it'll be faster if the inner selector is an ID) + if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && + Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + + ret = Sizzle.find( parts.shift(), context, contextXML ); + context = ret.expr ? + Sizzle.filter( ret.expr, ret.set )[0] : + ret.set[0]; + } + + if ( context ) { + ret = seed ? + { expr: parts.pop(), set: makeArray(seed) } : + Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); + + set = ret.expr ? + Sizzle.filter( ret.expr, ret.set ) : + ret.set; + + if ( parts.length > 0 ) { + checkSet = makeArray( set ); + + } else { + prune = false; + } + + while ( parts.length ) { + cur = parts.pop(); + pop = cur; + + if ( !Expr.relative[ cur ] ) { + cur = ""; + } else { + pop = parts.pop(); + } + + if ( pop == null ) { + pop = context; + } + + Expr.relative[ cur ]( checkSet, pop, contextXML ); + } + + } else { + checkSet = parts = []; + } + } + + if ( !checkSet ) { + checkSet = set; + } + + if ( !checkSet ) { + Sizzle.error( cur || selector ); + } + + if ( toString.call(checkSet) === "[object Array]" ) { + if ( !prune ) { + results.push.apply( results, checkSet ); + + } else if ( context && context.nodeType === 1 ) { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { + results.push( set[i] ); + } + } + + } else { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && checkSet[i].nodeType === 1 ) { + results.push( set[i] ); + } + } + } + + } else { + makeArray( checkSet, results ); + } + + if ( extra ) { + Sizzle( extra, origContext, results, seed ); + Sizzle.uniqueSort( results ); + } + + return results; +}; + +Sizzle.uniqueSort = function( results ) { + if ( sortOrder ) { + hasDuplicate = baseHasDuplicate; + results.sort( sortOrder ); + + if ( hasDuplicate ) { + for ( var i = 1; i < results.length; i++ ) { + if ( results[i] === results[ i - 1 ] ) { + results.splice( i--, 1 ); + } + } + } + } + + return results; +}; + +Sizzle.matches = function( expr, set ) { + return Sizzle( expr, null, null, set ); +}; + +Sizzle.matchesSelector = function( node, expr ) { + return Sizzle( expr, null, null, [node] ).length > 0; +}; + +Sizzle.find = function( expr, context, isXML ) { + var set, i, len, match, type, left; + + if ( !expr ) { + return []; + } + + for ( i = 0, len = Expr.order.length; i < len; i++ ) { + type = Expr.order[i]; + + if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { + left = match[1]; + match.splice( 1, 1 ); + + if ( left.substr( left.length - 1 ) !== "\\" ) { + match[1] = (match[1] || "").replace( rBackslash, "" ); + set = Expr.find[ type ]( match, context, isXML ); + + if ( set != null ) { + expr = expr.replace( Expr.match[ type ], "" ); + break; + } + } + } + } + + if ( !set ) { + set = typeof context.getElementsByTagName !== "undefined" ? + context.getElementsByTagName( "*" ) : + []; + } + + return { set: set, expr: expr }; +}; + +Sizzle.filter = function( expr, set, inplace, not ) { + var match, anyFound, + type, found, item, filter, left, + i, pass, + old = expr, + result = [], + curLoop = set, + isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); + + while ( expr && set.length ) { + for ( type in Expr.filter ) { + if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { + filter = Expr.filter[ type ]; + left = match[1]; + + anyFound = false; + + match.splice(1,1); + + if ( left.substr( left.length - 1 ) === "\\" ) { + continue; + } + + if ( curLoop === result ) { + result = []; + } + + if ( Expr.preFilter[ type ] ) { + match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); + + if ( !match ) { + anyFound = found = true; + + } else if ( match === true ) { + continue; + } + } + + if ( match ) { + for ( i = 0; (item = curLoop[i]) != null; i++ ) { + if ( item ) { + found = filter( item, match, i, curLoop ); + pass = not ^ found; + + if ( inplace && found != null ) { + if ( pass ) { + anyFound = true; + + } else { + curLoop[i] = false; + } + + } else if ( pass ) { + result.push( item ); + anyFound = true; + } + } + } + } + + if ( found !== undefined ) { + if ( !inplace ) { + curLoop = result; + } + + expr = expr.replace( Expr.match[ type ], "" ); + + if ( !anyFound ) { + return []; + } + + break; + } + } + } + + // Improper expression + if ( expr === old ) { + if ( anyFound == null ) { + Sizzle.error( expr ); + + } else { + break; + } + } + + old = expr; + } + + return curLoop; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Utility function for retreiving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +var getText = Sizzle.getText = function( elem ) { + var i, node, + nodeType = elem.nodeType, + ret = ""; + + if ( nodeType ) { + if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent || innerText for elements + if ( typeof elem.textContent === 'string' ) { + return elem.textContent; + } else if ( typeof elem.innerText === 'string' ) { + // Replace IE's carriage returns + return elem.innerText.replace( rReturn, '' ); + } else { + // Traverse it's children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + } else { + + // If no nodeType, this is expected to be an array + for ( i = 0; (node = elem[i]); i++ ) { + // Do not traverse comment nodes + if ( node.nodeType !== 8 ) { + ret += getText( node ); + } + } + } + return ret; +}; + +var Expr = Sizzle.selectors = { + order: [ "ID", "NAME", "TAG" ], + + match: { + ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, + ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/, + TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, + CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/, + POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, + PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ + }, + + leftMatch: {}, + + attrMap: { + "class": "className", + "for": "htmlFor" + }, + + attrHandle: { + href: function( elem ) { + return elem.getAttribute( "href" ); + }, + type: function( elem ) { + return elem.getAttribute( "type" ); + } + }, + + relative: { + "+": function(checkSet, part){ + var isPartStr = typeof part === "string", + isTag = isPartStr && !rNonWord.test( part ), + isPartStrNotTag = isPartStr && !isTag; + + if ( isTag ) { + part = part.toLowerCase(); + } + + for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { + if ( (elem = checkSet[i]) ) { + while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} + + checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? + elem || false : + elem === part; + } + } + + if ( isPartStrNotTag ) { + Sizzle.filter( part, checkSet, true ); + } + }, + + ">": function( checkSet, part ) { + var elem, + isPartStr = typeof part === "string", + i = 0, + l = checkSet.length; + + if ( isPartStr && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + var parent = elem.parentNode; + checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; + } + } + + } else { + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + checkSet[i] = isPartStr ? + elem.parentNode : + elem.parentNode === part; + } + } + + if ( isPartStr ) { + Sizzle.filter( part, checkSet, true ); + } + } + }, + + "": function(checkSet, part, isXML){ + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); + }, + + "~": function( checkSet, part, isXML ) { + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); + } + }, + + find: { + ID: function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }, + + NAME: function( match, context ) { + if ( typeof context.getElementsByName !== "undefined" ) { + var ret = [], + results = context.getElementsByName( match[1] ); + + for ( var i = 0, l = results.length; i < l; i++ ) { + if ( results[i].getAttribute("name") === match[1] ) { + ret.push( results[i] ); + } + } + + return ret.length === 0 ? null : ret; + } + }, + + TAG: function( match, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( match[1] ); + } + } + }, + preFilter: { + CLASS: function( match, curLoop, inplace, result, not, isXML ) { + match = " " + match[1].replace( rBackslash, "" ) + " "; + + if ( isXML ) { + return match; + } + + for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { + if ( elem ) { + if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) { + if ( !inplace ) { + result.push( elem ); + } + + } else if ( inplace ) { + curLoop[i] = false; + } + } + } + + return false; + }, + + ID: function( match ) { + return match[1].replace( rBackslash, "" ); + }, + + TAG: function( match, curLoop ) { + return match[1].replace( rBackslash, "" ).toLowerCase(); + }, + + CHILD: function( match ) { + if ( match[1] === "nth" ) { + if ( !match[2] ) { + Sizzle.error( match[0] ); + } + + match[2] = match[2].replace(/^\+|\s*/g, ''); + + // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' + var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec( + match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || + !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); + + // calculate the numbers (first)n+(last) including if they are negative + match[2] = (test[1] + (test[2] || 1)) - 0; + match[3] = test[3] - 0; + } + else if ( match[2] ) { + Sizzle.error( match[0] ); + } + + // TODO: Move to normal caching system + match[0] = done++; + + return match; + }, + + ATTR: function( match, curLoop, inplace, result, not, isXML ) { + var name = match[1] = match[1].replace( rBackslash, "" ); + + if ( !isXML && Expr.attrMap[name] ) { + match[1] = Expr.attrMap[name]; + } + + // Handle if an un-quoted value was used + match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" ); + + if ( match[2] === "~=" ) { + match[4] = " " + match[4] + " "; + } + + return match; + }, + + PSEUDO: function( match, curLoop, inplace, result, not ) { + if ( match[1] === "not" ) { + // If we're dealing with a complex expression, or a simple one + if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { + match[3] = Sizzle(match[3], null, null, curLoop); + + } else { + var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + + if ( !inplace ) { + result.push.apply( result, ret ); + } + + return false; + } + + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { + return true; + } + + return match; + }, + + POS: function( match ) { + match.unshift( true ); + + return match; + } + }, + + filters: { + enabled: function( elem ) { + return elem.disabled === false && elem.type !== "hidden"; + }, + + disabled: function( elem ) { + return elem.disabled === true; + }, + + checked: function( elem ) { + return elem.checked === true; + }, + + selected: function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + parent: function( elem ) { + return !!elem.firstChild; + }, + + empty: function( elem ) { + return !elem.firstChild; + }, + + has: function( elem, i, match ) { + return !!Sizzle( match[3], elem ).length; + }, + + header: function( elem ) { + return (/h\d/i).test( elem.nodeName ); + }, + + text: function( elem ) { + var attr = elem.getAttribute( "type" ), type = elem.type; + // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) + // use getAttribute instead to test this case + return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null ); + }, + + radio: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type; + }, + + checkbox: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type; + }, + + file: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "file" === elem.type; + }, + + password: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "password" === elem.type; + }, + + submit: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "submit" === elem.type; + }, + + image: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "image" === elem.type; + }, + + reset: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "reset" === elem.type; + }, + + button: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && "button" === elem.type || name === "button"; + }, + + input: function( elem ) { + return (/input|select|textarea|button/i).test( elem.nodeName ); + }, + + focus: function( elem ) { + return elem === elem.ownerDocument.activeElement; + } + }, + setFilters: { + first: function( elem, i ) { + return i === 0; + }, + + last: function( elem, i, match, array ) { + return i === array.length - 1; + }, + + even: function( elem, i ) { + return i % 2 === 0; + }, + + odd: function( elem, i ) { + return i % 2 === 1; + }, + + lt: function( elem, i, match ) { + return i < match[3] - 0; + }, + + gt: function( elem, i, match ) { + return i > match[3] - 0; + }, + + nth: function( elem, i, match ) { + return match[3] - 0 === i; + }, + + eq: function( elem, i, match ) { + return match[3] - 0 === i; + } + }, + filter: { + PSEUDO: function( elem, match, i, array ) { + var name = match[1], + filter = Expr.filters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + + } else if ( name === "contains" ) { + return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; + + } else if ( name === "not" ) { + var not = match[3]; + + for ( var j = 0, l = not.length; j < l; j++ ) { + if ( not[j] === elem ) { + return false; + } + } + + return true; + + } else { + Sizzle.error( name ); + } + }, + + CHILD: function( elem, match ) { + var first, last, + doneName, parent, cache, + count, diff, + type = match[1], + node = elem; + + switch ( type ) { + case "only": + case "first": + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + if ( type === "first" ) { + return true; + } + + node = elem; + + /* falls through */ + case "last": + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + return true; + + case "nth": + first = match[2]; + last = match[3]; + + if ( first === 1 && last === 0 ) { + return true; + } + + doneName = match[0]; + parent = elem.parentNode; + + if ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) { + count = 0; + + for ( node = parent.firstChild; node; node = node.nextSibling ) { + if ( node.nodeType === 1 ) { + node.nodeIndex = ++count; + } + } + + parent[ expando ] = doneName; + } + + diff = elem.nodeIndex - last; + + if ( first === 0 ) { + return diff === 0; + + } else { + return ( diff % first === 0 && diff / first >= 0 ); + } + } + }, + + ID: function( elem, match ) { + return elem.nodeType === 1 && elem.getAttribute("id") === match; + }, + + TAG: function( elem, match ) { + return (match === "*" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match; + }, + + CLASS: function( elem, match ) { + return (" " + (elem.className || elem.getAttribute("class")) + " ") + .indexOf( match ) > -1; + }, + + ATTR: function( elem, match ) { + var name = match[1], + result = Sizzle.attr ? + Sizzle.attr( elem, name ) : + Expr.attrHandle[ name ] ? + Expr.attrHandle[ name ]( elem ) : + elem[ name ] != null ? + elem[ name ] : + elem.getAttribute( name ), + value = result + "", + type = match[2], + check = match[4]; + + return result == null ? + type === "!=" : + !type && Sizzle.attr ? + result != null : + type === "=" ? + value === check : + type === "*=" ? + value.indexOf(check) >= 0 : + type === "~=" ? + (" " + value + " ").indexOf(check) >= 0 : + !check ? + value && result !== false : + type === "!=" ? + value !== check : + type === "^=" ? + value.indexOf(check) === 0 : + type === "$=" ? + value.substr(value.length - check.length) === check : + type === "|=" ? + value === check || value.substr(0, check.length + 1) === check + "-" : + false; + }, + + POS: function( elem, match, i, array ) { + var name = match[2], + filter = Expr.setFilters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + } + } + } +}; + +var origPOS = Expr.match.POS, + fescape = function(all, num){ + return "\\" + (num - 0 + 1); + }; + +for ( var type in Expr.match ) { + Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); + Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); +} +// Expose origPOS +// "global" as in regardless of relation to brackets/parens +Expr.match.globalPOS = origPOS; + +var makeArray = function( array, results ) { + array = Array.prototype.slice.call( array, 0 ); + + if ( results ) { + results.push.apply( results, array ); + return results; + } + + return array; +}; + +// Perform a simple check to determine if the browser is capable of +// converting a NodeList to an array using builtin methods. +// Also verifies that the returned array holds DOM nodes +// (which is not the case in the Blackberry browser) +try { + Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; + +// Provide a fallback method if it does not work +} catch( e ) { + makeArray = function( array, results ) { + var i = 0, + ret = results || []; + + if ( toString.call(array) === "[object Array]" ) { + Array.prototype.push.apply( ret, array ); + + } else { + if ( typeof array.length === "number" ) { + for ( var l = array.length; i < l; i++ ) { + ret.push( array[i] ); + } + + } else { + for ( ; array[i]; i++ ) { + ret.push( array[i] ); + } + } + } + + return ret; + }; +} + +var sortOrder, siblingCheck; + +if ( document.documentElement.compareDocumentPosition ) { + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { + return a.compareDocumentPosition ? -1 : 1; + } + + return a.compareDocumentPosition(b) & 4 ? -1 : 1; + }; + +} else { + sortOrder = function( a, b ) { + // The nodes are identical, we can exit early + if ( a === b ) { + hasDuplicate = true; + return 0; + + // Fallback to using sourceIndex (in IE) if it's available on both nodes + } else if ( a.sourceIndex && b.sourceIndex ) { + return a.sourceIndex - b.sourceIndex; + } + + var al, bl, + ap = [], + bp = [], + aup = a.parentNode, + bup = b.parentNode, + cur = aup; + + // If the nodes are siblings (or identical) we can do a quick check + if ( aup === bup ) { + return siblingCheck( a, b ); + + // If no parents were found then the nodes are disconnected + } else if ( !aup ) { + return -1; + + } else if ( !bup ) { + return 1; + } + + // Otherwise they're somewhere else in the tree so we need + // to build up a full list of the parentNodes for comparison + while ( cur ) { + ap.unshift( cur ); + cur = cur.parentNode; + } + + cur = bup; + + while ( cur ) { + bp.unshift( cur ); + cur = cur.parentNode; + } + + al = ap.length; + bl = bp.length; + + // Start walking down the tree looking for a discrepancy + for ( var i = 0; i < al && i < bl; i++ ) { + if ( ap[i] !== bp[i] ) { + return siblingCheck( ap[i], bp[i] ); + } + } + + // We ended someplace up the tree so do a sibling check + return i === al ? + siblingCheck( a, bp[i], -1 ) : + siblingCheck( ap[i], b, 1 ); + }; + + siblingCheck = function( a, b, ret ) { + if ( a === b ) { + return ret; + } + + var cur = a.nextSibling; + + while ( cur ) { + if ( cur === b ) { + return -1; + } + + cur = cur.nextSibling; + } + + return 1; + }; +} + +// Check to see if the browser returns elements by name when +// querying by getElementById (and provide a workaround) +(function(){ + // We're going to inject a fake input element with a specified name + var form = document.createElement("div"), + id = "script" + (new Date()).getTime(), + root = document.documentElement; + + form.innerHTML = ""; + + // Inject it into the root element, check its status, and remove it quickly + root.insertBefore( form, root.firstChild ); + + // The workaround has to do additional checks after a getElementById + // Which slows things down for other browsers (hence the branching) + if ( document.getElementById( id ) ) { + Expr.find.ID = function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + + return m ? + m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? + [m] : + undefined : + []; + } + }; + + Expr.filter.ID = function( elem, match ) { + var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + + return elem.nodeType === 1 && node && node.nodeValue === match; + }; + } + + root.removeChild( form ); + + // release memory in IE + root = form = null; +})(); + +(function(){ + // Check to see if the browser returns only elements + // when doing getElementsByTagName("*") + + // Create a fake element + var div = document.createElement("div"); + div.appendChild( document.createComment("") ); + + // Make sure no comments are found + if ( div.getElementsByTagName("*").length > 0 ) { + Expr.find.TAG = function( match, context ) { + var results = context.getElementsByTagName( match[1] ); + + // Filter out possible comments + if ( match[1] === "*" ) { + var tmp = []; + + for ( var i = 0; results[i]; i++ ) { + if ( results[i].nodeType === 1 ) { + tmp.push( results[i] ); + } + } + + results = tmp; + } + + return results; + }; + } + + // Check to see if an attribute returns normalized href attributes + div.innerHTML = ""; + + if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && + div.firstChild.getAttribute("href") !== "#" ) { + + Expr.attrHandle.href = function( elem ) { + return elem.getAttribute( "href", 2 ); + }; + } + + // release memory in IE + div = null; +})(); + +if ( document.querySelectorAll ) { + (function(){ + var oldSizzle = Sizzle, + div = document.createElement("div"), + id = "__sizzle__"; + + div.innerHTML = "

"; + + // Safari can't handle uppercase or unicode characters when + // in quirks mode. + if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { + return; + } + + Sizzle = function( query, context, extra, seed ) { + context = context || document; + + // Only use querySelectorAll on non-XML documents + // (ID selectors don't work in non-HTML documents) + if ( !seed && !Sizzle.isXML(context) ) { + // See if we find a selector to speed up + var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query ); + + if ( match && (context.nodeType === 1 || context.nodeType === 9) ) { + // Speed-up: Sizzle("TAG") + if ( match[1] ) { + return makeArray( context.getElementsByTagName( query ), extra ); + + // Speed-up: Sizzle(".CLASS") + } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) { + return makeArray( context.getElementsByClassName( match[2] ), extra ); + } + } + + if ( context.nodeType === 9 ) { + // Speed-up: Sizzle("body") + // The body element only exists once, optimize finding it + if ( query === "body" && context.body ) { + return makeArray( [ context.body ], extra ); + + // Speed-up: Sizzle("#ID") + } else if ( match && match[3] ) { + var elem = context.getElementById( match[3] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id === match[3] ) { + return makeArray( [ elem ], extra ); + } + + } else { + return makeArray( [], extra ); + } + } + + try { + return makeArray( context.querySelectorAll(query), extra ); + } catch(qsaError) {} + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + var oldContext = context, + old = context.getAttribute( "id" ), + nid = old || id, + hasParent = context.parentNode, + relativeHierarchySelector = /^\s*[+~]/.test( query ); + + if ( !old ) { + context.setAttribute( "id", nid ); + } else { + nid = nid.replace( /'/g, "\\$&" ); + } + if ( relativeHierarchySelector && hasParent ) { + context = context.parentNode; + } + + try { + if ( !relativeHierarchySelector || hasParent ) { + return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra ); + } + + } catch(pseudoError) { + } finally { + if ( !old ) { + oldContext.removeAttribute( "id" ); + } + } + } + } + + return oldSizzle(query, context, extra, seed); + }; + + for ( var prop in oldSizzle ) { + Sizzle[ prop ] = oldSizzle[ prop ]; + } + + // release memory in IE + div = null; + })(); +} + +(function(){ + var html = document.documentElement, + matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector; + + if ( matches ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9 fails this) + var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ), + pseudoWorks = false; + + try { + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( document.documentElement, "[test!='']:sizzle" ); + + } catch( pseudoError ) { + pseudoWorks = true; + } + + Sizzle.matchesSelector = function( node, expr ) { + // Make sure that attribute selectors are quoted + expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); + + if ( !Sizzle.isXML( node ) ) { + try { + if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { + var ret = matches.call( node, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || !disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9, so check for that + node.document && node.document.nodeType !== 11 ) { + return ret; + } + } + } catch(e) {} + } + + return Sizzle(expr, null, null, [node]).length > 0; + }; + } +})(); + +(function(){ + var div = document.createElement("div"); + + div.innerHTML = "
"; + + // Opera can't find a second classname (in 9.6) + // Also, make sure that getElementsByClassName actually exists + if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { + return; + } + + // Safari caches class attributes, doesn't catch changes (in 3.2) + div.lastChild.className = "e"; + + if ( div.getElementsByClassName("e").length === 1 ) { + return; + } + + Expr.order.splice(1, 0, "CLASS"); + Expr.find.CLASS = function( match, context, isXML ) { + if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { + return context.getElementsByClassName(match[1]); + } + }; + + // release memory in IE + div = null; +})(); + +function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem[ expando ] === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 && !isXML ){ + elem[ expando ] = doneName; + elem.sizset = i; + } + + if ( elem.nodeName.toLowerCase() === cur ) { + match = elem; + break; + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem[ expando ] === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 ) { + if ( !isXML ) { + elem[ expando ] = doneName; + elem.sizset = i; + } + + if ( typeof cur !== "string" ) { + if ( elem === cur ) { + match = true; + break; + } + + } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { + match = elem; + break; + } + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +if ( document.documentElement.contains ) { + Sizzle.contains = function( a, b ) { + return a !== b && (a.contains ? a.contains(b) : true); + }; + +} else if ( document.documentElement.compareDocumentPosition ) { + Sizzle.contains = function( a, b ) { + return !!(a.compareDocumentPosition(b) & 16); + }; + +} else { + Sizzle.contains = function() { + return false; + }; +} + +Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; + + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +var posProcess = function( selector, context, seed ) { + var match, + tmpSet = [], + later = "", + root = context.nodeType ? [context] : context; + + // Position selectors must be done after the filter + // And so must :not(positional) so we move all PSEUDOs to the end + while ( (match = Expr.match.PSEUDO.exec( selector )) ) { + later += match[0]; + selector = selector.replace( Expr.match.PSEUDO, "" ); + } + + selector = Expr.relative[selector] ? selector + "*" : selector; + + for ( var i = 0, l = root.length; i < l; i++ ) { + Sizzle( selector, root[i], tmpSet, seed ); + } + + return Sizzle.filter( later, tmpSet ); +}; + +// EXPOSE +// Override sizzle attribute retrieval +Sizzle.attr = jQuery.attr; +Sizzle.selectors.attrMap = {}; +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.filters; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})(); + + +var runtil = /Until$/, + rparentsprev = /^(?:parents|prevUntil|prevAll)/, + // Note: This RegExp should be improved, or likely pulled from Sizzle + rmultiselector = /,/, + isSimple = /^.[^:#\[\.,]*$/, + slice = Array.prototype.slice, + POS = jQuery.expr.match.globalPOS, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend({ + find: function( selector ) { + var self = this, + i, l; + + if ( typeof selector !== "string" ) { + return jQuery( selector ).filter(function() { + for ( i = 0, l = self.length; i < l; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }); + } + + var ret = this.pushStack( "", "find", selector ), + length, n, r; + + for ( i = 0, l = this.length; i < l; i++ ) { + length = ret.length; + jQuery.find( selector, this[i], ret ); + + if ( i > 0 ) { + // Make sure that the results are unique + for ( n = length; n < ret.length; n++ ) { + for ( r = 0; r < length; r++ ) { + if ( ret[r] === ret[n] ) { + ret.splice(n--, 1); + break; + } + } + } + } + } + + return ret; + }, + + has: function( target ) { + var targets = jQuery( target ); + return this.filter(function() { + for ( var i = 0, l = targets.length; i < l; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector, false), "not", selector); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector, true), "filter", selector ); + }, + + is: function( selector ) { + return !!selector && ( + typeof selector === "string" ? + // If this is a positional selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + POS.test( selector ) ? + jQuery( selector, this.context ).index( this[0] ) >= 0 : + jQuery.filter( selector, this ).length > 0 : + this.filter( selector ).length > 0 ); + }, + + closest: function( selectors, context ) { + var ret = [], i, l, cur = this[0]; + + // Array (deprecated as of jQuery 1.7) + if ( jQuery.isArray( selectors ) ) { + var level = 1; + + while ( cur && cur.ownerDocument && cur !== context ) { + for ( i = 0; i < selectors.length; i++ ) { + + if ( jQuery( cur ).is( selectors[ i ] ) ) { + ret.push({ selector: selectors[ i ], elem: cur, level: level }); + } + } + + cur = cur.parentNode; + level++; + } + + return ret; + } + + // String + var pos = POS.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( i = 0, l = this.length; i < l; i++ ) { + cur = this[i]; + + while ( cur ) { + if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { + ret.push( cur ); + break; + + } else { + cur = cur.parentNode; + if ( !cur || !cur.ownerDocument || cur === context || cur.nodeType === 11 ) { + break; + } + } + } + } + + ret = ret.length > 1 ? jQuery.unique( ret ) : ret; + + return this.pushStack( ret, "closest", selectors ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); + } + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context ) : + jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? + all : + jQuery.unique( all ) ); + }, + + andSelf: function() { + return this.add( this.prevObject ); + } +}); + +// A painfully simple check to see if an element is disconnected +// from a document (should be improved, where feasible). +function isDisconnected( node ) { + return !node || !node.parentNode || node.parentNode.nodeType === 11; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return jQuery.nth( elem, 2, "nextSibling" ); + }, + prev: function( elem ) { + return jQuery.nth( elem, 2, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.makeArray( elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( !runtil.test( name ) ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; + + if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + + return this.pushStack( ret, name, slice.call( arguments ).join(",") ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 ? + jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : + jQuery.find.matches(expr, elems); + }, + + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + nth: function( cur, result, dir, elem ) { + result = result || 1; + var num = 0; + + for ( ; cur; cur = cur[dir] ) { + if ( cur.nodeType === 1 && ++num === result ) { + break; + } + } + + return cur; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, keep ) { + + // Can't pass null or undefined to indexOf in Firefox 4 + // Set to 0 to skip string check + qualifier = qualifier || 0; + + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + var retVal = !!qualifier.call( elem, i, elem ); + return retVal === keep; + }); + + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem, i ) { + return ( elem === qualifier ) === keep; + }); + + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); + + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } + } + + return jQuery.grep(elements, function( elem, i ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; + }); +} + + + + +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, + rtagName = /<([\w:]+)/, + rtbody = /]", "i"), + // checked="checked" or checked + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, + rscriptType = /\/(java|ecma)script/i, + rcleanScript = /^\s*", "" ], + legend: [ 1, "
", "
" ], + thead: [ 1, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + col: [ 2, "", "
" ], + area: [ 1, "", "" ], + _default: [ 0, "", "" ] + }, + safeFragment = createSafeFragment( document ); + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// IE can't serialize and + + + + + + + + +
+
+
+
+ + +

Index

+ +
+ +
+ + +
+
+
+
+
+ + + + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_build/html/gnupg.html b/docs/_build/html/gnupg.html new file mode 100644 index 0000000..9606c42 --- /dev/null +++ b/docs/_build/html/gnupg.html @@ -0,0 +1,112 @@ + + + + + + + + + + gnupg Module — python-gnupg 0.3.1 documentation + + + + + + + + + + + + + + + +
+
+
+
+ +
+

gnupg Module¶

+
+ + +
+
+
+
+
+

Previous topic

+

Welcome to python-gnupg’s documentation!

+

Next topic

+

setup Module

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_build/html/index.html b/docs/_build/html/index.html new file mode 100644 index 0000000..76cf96d --- /dev/null +++ b/docs/_build/html/index.html @@ -0,0 +1,126 @@ + + + + + + + + + + Welcome to python-gnupg’s documentation! — python-gnupg 0.3.1 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

Welcome to python-gnupg’s documentation!¶

+

Contents:

+ +
+
+

Indices and tables¶

+ +
+ + +
+
+
+
+
+

Table Of Contents

+ + +

Next topic

+

gnupg Module

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_build/html/objects.inv b/docs/_build/html/objects.inv new file mode 100644 index 0000000000000000000000000000000000000000..3f83f00316d181310223513aa8ef6b0ac9418f95 GIT binary patch literal 210 zcmY#Z2rkIT%&Sny%qvUHE6FdaR47X=D$dN$Q!wIERtPA{&q_@$u~I0gEXl~v(@oDS zEl3B7he6a>DH!M(>lp$CLNZbnic)hEGxJhXixl$H6iPBOixtu`b5a$6baH-fK~ZXP zacYV}X>n#=xW|AIP#jVq4{ZF3p_FNmFq35~rl&6o^*}w~?AsQ>LoNe`bkviqc zGY#z@&z4VlwnVF@7tQu_b^ literal 0 HcmV?d00001 diff --git a/docs/_build/html/search.html b/docs/_build/html/search.html new file mode 100644 index 0000000..e5cef7f --- /dev/null +++ b/docs/_build/html/search.html @@ -0,0 +1,99 @@ + + + + + + + + + + Search — python-gnupg 0.3.1 documentation + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Search

+
+ +

+ Please activate JavaScript to enable the search + functionality. +

+
+

+ From here you can search these documents. Enter your search + words into the box below and click "search". Note that the search + function will automatically search for all of the words. Pages + containing fewer words won't appear in the result list. +

+
+ + + +
+ +
+ +
+ +
+
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_build/html/searchindex.js b/docs/_build/html/searchindex.js new file mode 100644 index 0000000..57d045d --- /dev/null +++ b/docs/_build/html/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({objects:{},terms:{index:0,search:0,welcom:0,python:0,modul:[0,1,2,3],indic:0,content:0,test_gnupg:[0,2],gnupg:[0,3],tabl:0,setup:[0,1],document:0,page:0},objtypes:{},titles:["Welcome to python-gnupg’s documentation!","setup Module","test_gnupg Module","gnupg Module"],objnames:{},filenames:["index","setup","test_gnupg","gnupg"]}) \ No newline at end of file diff --git a/docs/_build/html/setup.html b/docs/_build/html/setup.html new file mode 100644 index 0000000..1063ae6 --- /dev/null +++ b/docs/_build/html/setup.html @@ -0,0 +1,112 @@ + + + + + + + + + + setup Module — python-gnupg 0.3.1 documentation + + + + + + + + + + + + + + + +
+
+
+
+ +
+

setup Module¶

+
+ + +
+
+
+
+
+

Previous topic

+

gnupg Module

+

Next topic

+

test_gnupg Module

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_build/html/test_gnupg.html b/docs/_build/html/test_gnupg.html new file mode 100644 index 0000000..80bff31 --- /dev/null +++ b/docs/_build/html/test_gnupg.html @@ -0,0 +1,102 @@ + + + + + + + + + + test_gnupg Module — python-gnupg 0.3.1 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

test_gnupg Module¶

+
+ + +
+
+
+
+
+

Previous topic

+

setup Module

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..ef80ed2 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,286 @@ +# -*- coding: utf-8 -*- +# +# python-gnupg documentation build configuration file, created by +# sphinx-quickstart on Fri Apr 5 22:38:47 2013. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# 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 +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath('./../gnupg.py')) +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'python-gnupg' +copyright = u'2013, Isis Agora Lovecruft' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.3.1' +# The full version, including alpha/beta/rc tags. +release = '0.3.1' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'python-gnupgdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'python-gnupg.tex', u'python-gnupg Documentation', + u'Isis Agora Lovecruft', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'python-gnupg', u'python-gnupg Documentation', + [u'Isis Agora Lovecruft'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'python-gnupg', u'python-gnupg Documentation', + u'Isis Agora Lovecruft', 'python-gnupg', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + + +# -- Options for Epub output --------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = u'python-gnupg' +epub_author = u'Isis Agora Lovecruft' +epub_publisher = u'Isis Agora Lovecruft' +epub_copyright = u'2013, Isis Agora Lovecruft' + +# The language of the text. It defaults to the language option +# or en if the language is not set. +#epub_language = '' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +#epub_scheme = '' + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +#epub_identifier = '' + +# A unique identification for the text. +#epub_uid = '' + +# A tuple containing the cover image and cover page html template filenames. +#epub_cover = () + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_pre_files = [] + +# HTML files shat should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_post_files = [] + +# A list of files that should not be packed into the epub file. +#epub_exclude_files = [] + +# The depth of the table of contents in toc.ncx. +#epub_tocdepth = 3 + +# Allow duplicate toc entries. +#epub_tocdup = True diff --git a/docs/gnupg.rst b/docs/gnupg.rst new file mode 100644 index 0000000..0b159f9 --- /dev/null +++ b/docs/gnupg.rst @@ -0,0 +1,7 @@ +gnupg Module +============ + +.. automodule:: gnupg + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..d2b196d --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,25 @@ +.. python-gnupg documentation master file, created by + sphinx-quickstart on Fri Apr 5 22:38:47 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to python-gnupg's documentation! +======================================== + +Contents: + +.. toctree:: + :maxdepth: 4 + + gnupg + setup + test_gnupg + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..fa3950f --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,190 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\python-gnupg.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\python-gnupg.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/docs/setup.rst b/docs/setup.rst new file mode 100644 index 0000000..e3c5b3d --- /dev/null +++ b/docs/setup.rst @@ -0,0 +1,7 @@ +setup Module +============ + +.. automodule:: setup + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/test_gnupg.rst b/docs/test_gnupg.rst new file mode 100644 index 0000000..40de5ba --- /dev/null +++ b/docs/test_gnupg.rst @@ -0,0 +1,7 @@ +test_gnupg Module +================= + +.. automodule:: test_gnupg + :members: + :undoc-members: + :show-inheritance: From 1ed011a0dbaac35c3771f2e6865aabbefca04897 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 5 Apr 2013 23:02:14 +0000 Subject: [PATCH 008/397] Update Sphinx documentation config for autodoc and for module path discovery. --- docs/_build/doctrees/environment.pickle | Bin 5529 -> 0 bytes docs/_build/doctrees/gnupg.doctree | Bin 2844 -> 0 bytes docs/_build/doctrees/index.doctree | Bin 5533 -> 0 bytes docs/_build/doctrees/setup.doctree | Bin 2844 -> 0 bytes docs/_build/doctrees/test_gnupg.doctree | Bin 2884 -> 0 bytes docs/_build/html/.buildinfo | 4 - docs/_build/html/_sources/gnupg.txt | 7 - docs/_build/html/_sources/index.txt | 25 - docs/_build/html/_sources/setup.txt | 7 - docs/_build/html/_sources/test_gnupg.txt | 7 - docs/_build/html/_static/ajax-loader.gif | Bin 673 -> 0 bytes docs/_build/html/_static/basic.css | 540 -- docs/_build/html/_static/comment-bright.png | Bin 3500 -> 0 bytes docs/_build/html/_static/comment-close.png | Bin 3578 -> 0 bytes docs/_build/html/_static/comment.png | Bin 3445 -> 0 bytes docs/_build/html/_static/default.css | 256 - docs/_build/html/_static/doctools.js | 247 - docs/_build/html/_static/down-pressed.png | Bin 368 -> 0 bytes docs/_build/html/_static/down.png | Bin 363 -> 0 bytes docs/_build/html/_static/file.png | Bin 392 -> 0 bytes docs/_build/html/_static/jquery.js | 9404 ------------------- docs/_build/html/_static/minus.png | Bin 199 -> 0 bytes docs/_build/html/_static/plus.png | Bin 199 -> 0 bytes docs/_build/html/_static/pygments.css | 62 - docs/_build/html/_static/searchtools.js | 560 -- docs/_build/html/_static/sidebar.js | 151 - docs/_build/html/_static/underscore.js | 1226 --- docs/_build/html/_static/up-pressed.png | Bin 372 -> 0 bytes docs/_build/html/_static/up.png | Bin 363 -> 0 bytes docs/_build/html/_static/websupport.js | 808 -- docs/_build/html/genindex.html | 95 - docs/_build/html/gnupg.html | 112 - docs/_build/html/index.html | 126 - docs/_build/html/objects.inv | Bin 210 -> 0 bytes docs/_build/html/search.html | 99 - docs/_build/html/searchindex.js | 1 - docs/_build/html/setup.html | 112 - docs/_build/html/test_gnupg.html | 102 - docs/conf.py | 8 +- 39 files changed, 7 insertions(+), 13952 deletions(-) delete mode 100644 docs/_build/doctrees/environment.pickle delete mode 100644 docs/_build/doctrees/gnupg.doctree delete mode 100644 docs/_build/doctrees/index.doctree delete mode 100644 docs/_build/doctrees/setup.doctree delete mode 100644 docs/_build/doctrees/test_gnupg.doctree delete mode 100644 docs/_build/html/.buildinfo delete mode 100644 docs/_build/html/_sources/gnupg.txt delete mode 100644 docs/_build/html/_sources/index.txt delete mode 100644 docs/_build/html/_sources/setup.txt delete mode 100644 docs/_build/html/_sources/test_gnupg.txt delete mode 100644 docs/_build/html/_static/ajax-loader.gif delete mode 100644 docs/_build/html/_static/basic.css delete mode 100644 docs/_build/html/_static/comment-bright.png delete mode 100644 docs/_build/html/_static/comment-close.png delete mode 100644 docs/_build/html/_static/comment.png delete mode 100644 docs/_build/html/_static/default.css delete mode 100644 docs/_build/html/_static/doctools.js delete mode 100644 docs/_build/html/_static/down-pressed.png delete mode 100644 docs/_build/html/_static/down.png delete mode 100644 docs/_build/html/_static/file.png delete mode 100644 docs/_build/html/_static/jquery.js delete mode 100644 docs/_build/html/_static/minus.png delete mode 100644 docs/_build/html/_static/plus.png delete mode 100644 docs/_build/html/_static/pygments.css delete mode 100644 docs/_build/html/_static/searchtools.js delete mode 100644 docs/_build/html/_static/sidebar.js delete mode 100644 docs/_build/html/_static/underscore.js delete mode 100644 docs/_build/html/_static/up-pressed.png delete mode 100644 docs/_build/html/_static/up.png delete mode 100644 docs/_build/html/_static/websupport.js delete mode 100644 docs/_build/html/genindex.html delete mode 100644 docs/_build/html/gnupg.html delete mode 100644 docs/_build/html/index.html delete mode 100644 docs/_build/html/objects.inv delete mode 100644 docs/_build/html/search.html delete mode 100644 docs/_build/html/searchindex.js delete mode 100644 docs/_build/html/setup.html delete mode 100644 docs/_build/html/test_gnupg.html diff --git a/docs/_build/doctrees/environment.pickle b/docs/_build/doctrees/environment.pickle deleted file mode 100644 index 8d2c9d7140c5359df1409329e8bc97777d69daf2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5529 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 diff --git a/docs/_build/doctrees/index.doctree b/docs/_build/doctrees/index.doctree deleted file mode 100644 index 95318c2ef1b356260adaf56a539098ab5dadf836..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5533 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 diff --git a/docs/_build/html/_static/basic.css b/docs/_build/html/_static/basic.css deleted file mode 100644 index 43e8baf..0000000 --- a/docs/_build/html/_static/basic.css +++ /dev/null @@ -1,540 +0,0 @@ -/* - * basic.css - * ~~~~~~~~~ - * - * Sphinx stylesheet -- basic theme. - * - * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -/* -- main layout ----------------------------------------------------------- */ - -div.clearer { - clear: both; -} - -/* -- relbar ---------------------------------------------------------------- */ - -div.related { - width: 100%; - font-size: 90%; -} - -div.related h3 { - display: none; -} - -div.related ul { - margin: 0; - padding: 0 0 0 10px; - list-style: none; -} - -div.related li { - display: inline; -} - -div.related li.right { - float: right; - margin-right: 5px; -} - -/* -- sidebar --------------------------------------------------------------- */ - -div.sphinxsidebarwrapper { - padding: 10px 5px 0 10px; -} - -div.sphinxsidebar { - float: left; - width: 230px; - margin-left: -100%; - font-size: 90%; -} - -div.sphinxsidebar ul { - list-style: none; -} - -div.sphinxsidebar ul ul, -div.sphinxsidebar ul.want-points { - margin-left: 20px; - list-style: square; -} - -div.sphinxsidebar ul ul { - margin-top: 0; - margin-bottom: 0; -} - -div.sphinxsidebar form { - margin-top: 10px; -} - -div.sphinxsidebar input { - border: 1px solid #98dbcc; - font-family: sans-serif; - font-size: 1em; -} - -div.sphinxsidebar #searchbox input[type="text"] { - width: 170px; -} - -div.sphinxsidebar #searchbox input[type="submit"] { - width: 30px; -} - -img { - border: 0; -} - -/* -- search page ----------------------------------------------------------- */ - -ul.search { - margin: 10px 0 0 20px; - padding: 0; -} - -ul.search li { - padding: 5px 0 5px 20px; - background-image: url(file.png); - background-repeat: no-repeat; - background-position: 0 7px; -} - -ul.search li a { - font-weight: bold; -} - -ul.search li div.context { - color: #888; - margin: 2px 0 0 30px; - text-align: left; -} - -ul.keywordmatches li.goodmatch a { - font-weight: bold; -} - -/* -- index page ------------------------------------------------------------ */ - -table.contentstable { - width: 90%; -} - -table.contentstable p.biglink { - line-height: 150%; -} - -a.biglink { - font-size: 1.3em; -} - -span.linkdescr { - font-style: italic; - padding-top: 5px; - font-size: 90%; -} - -/* -- general index --------------------------------------------------------- */ - -table.indextable { - width: 100%; -} - -table.indextable td { - text-align: left; - vertical-align: top; -} - -table.indextable dl, table.indextable dd { - margin-top: 0; - margin-bottom: 0; -} - -table.indextable tr.pcap { - height: 10px; -} - -table.indextable tr.cap { - margin-top: 10px; - background-color: #f2f2f2; -} - -img.toggler { - margin-right: 3px; - margin-top: 3px; - cursor: pointer; -} - -div.modindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -div.genindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -/* -- general body styles --------------------------------------------------- */ - -a.headerlink { - visibility: hidden; -} - -h1:hover > a.headerlink, -h2:hover > a.headerlink, -h3:hover > a.headerlink, -h4:hover > a.headerlink, -h5:hover > a.headerlink, -h6:hover > a.headerlink, -dt:hover > a.headerlink { - visibility: visible; -} - -div.body p.caption { - text-align: inherit; -} - -div.body td { - text-align: left; -} - -.field-list ul { - padding-left: 1em; -} - -.first { - margin-top: 0 !important; -} - -p.rubric { - margin-top: 30px; - font-weight: bold; -} - -img.align-left, .figure.align-left, object.align-left { - clear: left; - float: left; - margin-right: 1em; -} - -img.align-right, .figure.align-right, object.align-right { - clear: right; - float: right; - margin-left: 1em; -} - -img.align-center, .figure.align-center, object.align-center { - display: block; - margin-left: auto; - margin-right: auto; -} - -.align-left { - text-align: left; -} - -.align-center { - text-align: center; -} - -.align-right { - text-align: right; -} - -/* -- sidebars -------------------------------------------------------------- */ - -div.sidebar { - margin: 0 0 0.5em 1em; - border: 1px solid #ddb; - padding: 7px 7px 0 7px; - background-color: #ffe; - width: 40%; - float: right; -} - -p.sidebar-title { - font-weight: bold; -} - -/* -- topics ---------------------------------------------------------------- */ - -div.topic { - border: 1px solid #ccc; - padding: 7px 7px 0 7px; - margin: 10px 0 10px 0; -} - -p.topic-title { - font-size: 1.1em; - font-weight: bold; - margin-top: 10px; -} - -/* -- admonitions ----------------------------------------------------------- */ - -div.admonition { - margin-top: 10px; - margin-bottom: 10px; - padding: 7px; -} - -div.admonition dt { - font-weight: bold; -} - -div.admonition dl { - margin-bottom: 0; -} - -p.admonition-title { - margin: 0px 10px 5px 0px; - font-weight: bold; -} - -div.body p.centered { - text-align: center; - margin-top: 25px; -} - -/* -- tables ---------------------------------------------------------------- */ - -table.docutils { - border: 0; - border-collapse: collapse; -} - -table.docutils td, table.docutils th { - padding: 1px 8px 1px 5px; - border-top: 0; - border-left: 0; - border-right: 0; - border-bottom: 1px solid #aaa; -} - -table.field-list td, table.field-list th { - border: 0 !important; -} - -table.footnote td, table.footnote th { - border: 0 !important; -} - -th { - text-align: left; - padding-right: 5px; -} - -table.citation { - border-left: solid 1px gray; - margin-left: 1px; -} - -table.citation td { - border-bottom: none; -} - -/* -- other body styles ----------------------------------------------------- */ - -ol.arabic { - list-style: decimal; -} - -ol.loweralpha { - list-style: lower-alpha; -} - -ol.upperalpha { - list-style: upper-alpha; -} - -ol.lowerroman { - list-style: lower-roman; -} - -ol.upperroman { - list-style: upper-roman; -} - -dl { - margin-bottom: 15px; -} - -dd p { - margin-top: 0px; -} - -dd ul, dd table { - margin-bottom: 10px; -} - -dd { - margin-top: 3px; - margin-bottom: 10px; - margin-left: 30px; -} - -dt:target, .highlighted { - background-color: #fbe54e; -} - -dl.glossary dt { - font-weight: bold; - font-size: 1.1em; -} - -.field-list ul { - margin: 0; - padding-left: 1em; -} - -.field-list p { - margin: 0; -} - -.refcount { - color: #060; -} - -.optional { - font-size: 1.3em; -} - -.versionmodified { - font-style: italic; -} - -.system-message { - background-color: #fda; - padding: 5px; - border: 3px solid red; -} - -.footnote:target { - background-color: #ffa; -} - -.line-block { - display: block; - margin-top: 1em; - margin-bottom: 1em; -} - -.line-block .line-block { - margin-top: 0; - margin-bottom: 0; - margin-left: 1.5em; -} - -.guilabel, .menuselection { - font-family: sans-serif; -} - -.accelerator { - text-decoration: underline; -} - -.classifier { - font-style: oblique; -} - -abbr, acronym { - border-bottom: dotted 1px; - cursor: help; -} - -/* -- code displays --------------------------------------------------------- */ - -pre { - overflow: auto; - overflow-y: hidden; /* fixes display issues on Chrome browsers */ -} - -td.linenos pre { - padding: 5px 0px; - border: 0; - background-color: transparent; - color: #aaa; -} - -table.highlighttable { - margin-left: 0.5em; -} - -table.highlighttable td { - padding: 0 0.5em 0 0.5em; -} - -tt.descname { - background-color: transparent; - font-weight: bold; - font-size: 1.2em; -} - -tt.descclassname { - background-color: transparent; -} - -tt.xref, a tt { - background-color: transparent; - font-weight: bold; -} - -h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { - background-color: transparent; -} - -.viewcode-link { - float: right; -} - -.viewcode-back { - float: right; - font-family: sans-serif; -} - -div.viewcode-block:target { - margin: -1px -10px; - padding: 0 10px; -} - -/* -- math display ---------------------------------------------------------- */ - -img.math { - vertical-align: middle; -} - -div.body div.math p { - text-align: center; -} - -span.eqno { - float: right; -} - -/* -- printout stylesheet --------------------------------------------------- */ - -@media print { - div.document, - div.documentwrapper, - div.bodywrapper { - margin: 0 !important; - width: 100%; - } - - div.sphinxsidebar, - div.related, - div.footer, - #top-link { - display: none; - } -} \ No newline at end of file diff --git a/docs/_build/html/_static/comment-bright.png b/docs/_build/html/_static/comment-bright.png deleted file mode 100644 index 551517b8c83b76f734ff791f847829a760ad1903..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3500 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 A2LJ#7 diff --git a/docs/_build/html/_static/comment.png b/docs/_build/html/_static/comment.png deleted file mode 100644 index 92feb52b8824c6b0f59b658b1196c61de9162a95..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3445 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 Xn!ZpXD%P(H00000NkvXXu0mjf= 0 && !jQuery(node.parentNode).hasClass(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.initIndexTable(); - }, - - /** - * 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 safely 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() { - $('\u00B6'). - attr('href', '#' + this.id). - attr('title', _('Permalink to this headline')). - appendTo(this); - }); - $('dt[id]').each(function() { - $('\u00B6'). - 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(), 'highlighted'); - }); - }, 10); - $('') - .appendTo($('#searchbox')); - } - }, - - /** - * init the domain index toggle buttons - */ - initIndexTable : function() { - var togglers = $('img.toggler').click(function() { - var src = $(this).attr('src'); - var idnum = $(this).attr('id').substr(7); - $('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_INDEX) { - togglers.click(); - } - }, - - /** - * helper function to hide the search marks again - */ - hideSearchWords : function() { - $('#searchbox .highlight-link').fadeOut(300); - $('span.highlighted').removeClass('highlighted'); - }, - - /** - * 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(); -}); diff --git a/docs/_build/html/_static/down-pressed.png b/docs/_build/html/_static/down-pressed.png deleted file mode 100644 index 6f7ad782782e4f8e39b0c6e15c7344700cdd2527..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 368 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6U4S$Y z{B+)352QE?JR*yM+OLB!qm#z$3ZNi+iKnkC`z>}Z23@f-Ava~9&<9T!#}JFtXD=!G zGdl{fK6ro2OGiOl+hKvH6i=D3%%Y^j`yIkRn!8O>@bG)IQR0{Kf+mxNd=_WScA8u_ z3;8(7x2){m9`nt+U(Nab&1G)!{`SPVpDX$w8McLTzAJ39wprG3p4XLq$06M`%}2Yk zRPPsbES*dnYm1wkGL;iioAUB*Or2kz6(-M_r_#Me-`{mj$Z%( diff --git a/docs/_build/html/_static/down.png b/docs/_build/html/_static/down.png deleted file mode 100644 index 3003a88770de3977d47a2ba69893436a2860f9e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 363 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6U4S$Y z{B+)352QE?JR*yM+OLB!qm#z$3ZNi+iKnkC`z>}xaV3tUZ$qnrLa#kt978NlpS`ru z&)HFc^}^>{UOEce+71h5nn>6&w6A!ieNbu1wh)UGh{8~et^#oZ1# z>T7oM=FZ~xXWnTo{qnXm$ZLOlqGswI_m2{XwVK)IJmBjW{J3-B3x@C=M{ShWt#fYS9M?R;8K$~YwlIqwf>VA7q=YKcwf2DS4Zj5inDKXXB1zl=(YO3ST6~rDq)&z z*o>z)=hxrfG-cDBW0G$!?6{M<$@{_4{m1o%Ub!naEtn|@^frU1tDnm{r-UW|!^@B8 diff --git a/docs/_build/html/_static/file.png b/docs/_build/html/_static/file.png deleted file mode 100644 index d18082e397e7e54f20721af768c4c2983258f1b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 392 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmP$HyOL$D9)yc9|lc|nKf<9@eUiWd>3GuTC!a5vdfWYEazjncPj5ZQX%+1 zt8B*4=d)!cdDz4wr^#OMYfqGz$1LDFF>|#>*O?AGil(WEs?wLLy{Gj2J_@opDm%`dlax3yA*@*N$G&*ukFv>P8+2CBWO(qz zD0k1@kN>hhb1_6`&wrCswzINE(evt-5C1B^STi2@PmdKI;Vst0PQB6!2kdN diff --git a/docs/_build/html/_static/jquery.js b/docs/_build/html/_static/jquery.js deleted file mode 100644 index 96d660c..0000000 --- a/docs/_build/html/_static/jquery.js +++ /dev/null @@ -1,9404 +0,0 @@ -/*! - * jQuery JavaScript Library v1.7.2 - * http://jquery.com/ - * - * Copyright 2011, John Resig - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * Includes Sizzle.js - * http://sizzlejs.com/ - * Copyright 2011, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * - * Date: Thu Nov 15 18:28:24 BRST 2012 - */ -(function( window, undefined ) { - -// Use the correct document accordingly with window argument (sandbox) -var document = window.document, - navigator = window.navigator, - location = window.location; -var jQuery = (function() { - -// Define a local copy of jQuery -var jQuery = function( selector, context ) { - // The jQuery object is actually just the init constructor 'enhanced' - return new jQuery.fn.init( selector, context, rootjQuery ); - }, - - // Map over jQuery in case of overwrite - _jQuery = window.jQuery, - - // Map over the $ in case of overwrite - _$ = window.$, - - // A central reference to the root jQuery(document) - rootjQuery, - - // A simple way to check for HTML strings or ID strings - // Prioritize #id over to avoid XSS via location.hash (#9521) - quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/, - - // Check if a string has a non-whitespace character in it - rnotwhite = /\S/, - - // Used for trimming whitespace - trimLeft = /^\s+/, - trimRight = /\s+$/, - - // Match a standalone tag - rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, - - // JSON RegExp - rvalidchars = /^[\],:{}\s]*$/, - rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, - rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, - rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, - - // Useragent RegExp - rwebkit = /(webkit)[ \/]([\w.]+)/, - ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/, - rmsie = /(msie) ([\w.]+)/, - rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, - - // Matches dashed string for camelizing - rdashAlpha = /-([a-z]|[0-9])/ig, - rmsPrefix = /^-ms-/, - - // Used by jQuery.camelCase as callback to replace() - fcamelCase = function( all, letter ) { - return ( letter + "" ).toUpperCase(); - }, - - // Keep a UserAgent string for use with jQuery.browser - userAgent = navigator.userAgent, - - // For matching the engine and version of the browser - browserMatch, - - // The deferred used on DOM ready - readyList, - - // The ready event handler - DOMContentLoaded, - - // Save a reference to some core methods - toString = Object.prototype.toString, - hasOwn = Object.prototype.hasOwnProperty, - push = Array.prototype.push, - slice = Array.prototype.slice, - trim = String.prototype.trim, - indexOf = Array.prototype.indexOf, - - // [[Class]] -> type pairs - class2type = {}; - -jQuery.fn = jQuery.prototype = { - constructor: jQuery, - init: function( selector, context, rootjQuery ) { - var match, elem, ret, doc; - - // Handle $(""), $(null), or $(undefined) - if ( !selector ) { - return this; - } - - // Handle $(DOMElement) - if ( selector.nodeType ) { - this.context = this[0] = selector; - this.length = 1; - return this; - } - - // The body element only exists once, optimize finding it - if ( selector === "body" && !context && document.body ) { - this.context = document; - this[0] = document.body; - this.selector = selector; - this.length = 1; - return this; - } - - // Handle HTML strings - if ( typeof selector === "string" ) { - // Are we dealing with HTML string or an ID? - if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { - // Assume that strings that start and end with <> are HTML and skip the regex check - match = [ null, selector, null ]; - - } else { - match = quickExpr.exec( selector ); - } - - // Verify a match, and that no context was specified for #id - if ( match && (match[1] || !context) ) { - - // HANDLE: $(html) -> $(array) - if ( match[1] ) { - context = context instanceof jQuery ? context[0] : context; - doc = ( context ? context.ownerDocument || context : document ); - - // If a single string is passed in and it's a single tag - // just do a createElement and skip the rest - ret = rsingleTag.exec( selector ); - - if ( ret ) { - if ( jQuery.isPlainObject( context ) ) { - selector = [ document.createElement( ret[1] ) ]; - jQuery.fn.attr.call( selector, context, true ); - - } else { - selector = [ doc.createElement( ret[1] ) ]; - } - - } else { - ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); - selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes; - } - - return jQuery.merge( this, selector ); - - // HANDLE: $("#id") - } else { - elem = document.getElementById( match[2] ); - - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE and Opera return items - // by name instead of ID - if ( elem.id !== match[2] ) { - return rootjQuery.find( selector ); - } - - // Otherwise, we inject the element directly into the jQuery object - this.length = 1; - this[0] = elem; - } - - this.context = document; - this.selector = selector; - return this; - } - - // HANDLE: $(expr, $(...)) - } else if ( !context || context.jquery ) { - return ( context || rootjQuery ).find( selector ); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return this.constructor( context ).find( selector ); - } - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( jQuery.isFunction( selector ) ) { - return rootjQuery.ready( selector ); - } - - if ( selector.selector !== undefined ) { - this.selector = selector.selector; - this.context = selector.context; - } - - return jQuery.makeArray( selector, this ); - }, - - // Start with an empty selector - selector: "", - - // The current version of jQuery being used - jquery: "1.7.2", - - // The default length of a jQuery object is 0 - length: 0, - - // The number of elements contained in the matched element set - size: function() { - return this.length; - }, - - toArray: function() { - return slice.call( this, 0 ); - }, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function( num ) { - return num == null ? - - // Return a 'clean' array - this.toArray() : - - // Return just the object - ( num < 0 ? this[ this.length + num ] : this[ num ] ); - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function( elems, name, selector ) { - // Build a new jQuery matched element set - var ret = this.constructor(); - - if ( jQuery.isArray( elems ) ) { - push.apply( ret, elems ); - - } else { - jQuery.merge( ret, elems ); - } - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - - ret.context = this.context; - - if ( name === "find" ) { - ret.selector = this.selector + ( this.selector ? " " : "" ) + selector; - } else if ( name ) { - ret.selector = this.selector + "." + name + "(" + selector + ")"; - } - - // Return the newly-formed element set - return ret; - }, - - // Execute a callback for every element in the matched set. - // (You can seed the arguments with an array of args, but this is - // only used internally.) - each: function( callback, args ) { - return jQuery.each( this, callback, args ); - }, - - ready: function( fn ) { - // Attach the listeners - jQuery.bindReady(); - - // Add the callback - readyList.add( fn ); - - return this; - }, - - eq: function( i ) { - i = +i; - return i === -1 ? - this.slice( i ) : - this.slice( i, i + 1 ); - }, - - first: function() { - return this.eq( 0 ); - }, - - last: function() { - return this.eq( -1 ); - }, - - slice: function() { - return this.pushStack( slice.apply( this, arguments ), - "slice", slice.call(arguments).join(",") ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map(this, function( elem, i ) { - return callback.call( elem, i, elem ); - })); - }, - - end: function() { - return this.prevObject || this.constructor(null); - }, - - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: push, - sort: [].sort, - splice: [].splice -}; - -// Give the init function the jQuery prototype for later instantiation -jQuery.fn.init.prototype = jQuery.fn; - -jQuery.extend = jQuery.fn.extend = function() { - var options, name, src, copy, copyIsArray, clone, - target = arguments[0] || {}, - i = 1, - length = arguments.length, - deep = false; - - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - target = arguments[1] || {}; - // skip the boolean and the target - i = 2; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !jQuery.isFunction(target) ) { - target = {}; - } - - // extend jQuery itself if only one argument is passed - if ( length === i ) { - target = this; - --i; - } - - for ( ; i < length; i++ ) { - // Only deal with non-null/undefined values - if ( (options = arguments[ i ]) != null ) { - // Extend the base object - for ( name in options ) { - src = target[ name ]; - copy = options[ name ]; - - // Prevent never-ending loop - if ( target === copy ) { - continue; - } - - // Recurse if we're merging plain objects or arrays - if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { - if ( copyIsArray ) { - copyIsArray = false; - clone = src && jQuery.isArray(src) ? src : []; - - } else { - clone = src && jQuery.isPlainObject(src) ? src : {}; - } - - // Never move original objects, clone them - target[ name ] = jQuery.extend( deep, clone, copy ); - - // Don't bring in undefined values - } else if ( copy !== undefined ) { - target[ name ] = copy; - } - } - } - } - - // Return the modified object - return target; -}; - -jQuery.extend({ - noConflict: function( deep ) { - if ( window.$ === jQuery ) { - window.$ = _$; - } - - if ( deep && window.jQuery === jQuery ) { - window.jQuery = _jQuery; - } - - return jQuery; - }, - - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, - - // A counter to track how many items to wait for before - // the ready event fires. See #6781 - readyWait: 1, - - // Hold (or release) the ready event - holdReady: function( hold ) { - if ( hold ) { - jQuery.readyWait++; - } else { - jQuery.ready( true ); - } - }, - - // Handle when the DOM is ready - ready: function( wait ) { - // Either a released hold or an DOMready/load event and not yet ready - if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) { - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if ( !document.body ) { - return setTimeout( jQuery.ready, 1 ); - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if ( wait !== true && --jQuery.readyWait > 0 ) { - return; - } - - // If there are functions bound, to execute - readyList.fireWith( document, [ jQuery ] ); - - // Trigger any bound ready events - if ( jQuery.fn.trigger ) { - jQuery( document ).trigger( "ready" ).off( "ready" ); - } - } - }, - - bindReady: function() { - if ( readyList ) { - return; - } - - readyList = jQuery.Callbacks( "once memory" ); - - // Catch cases where $(document).ready() is called after the - // browser event has already occurred. - if ( document.readyState === "complete" ) { - // Handle it asynchronously to allow scripts the opportunity to delay ready - return setTimeout( jQuery.ready, 1 ); - } - - // Mozilla, Opera and webkit nightlies currently support this event - if ( document.addEventListener ) { - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", jQuery.ready, false ); - - // If IE event model is used - } else if ( document.attachEvent ) { - // ensure firing before onload, - // maybe late but safe also for iframes - document.attachEvent( "onreadystatechange", DOMContentLoaded ); - - // A fallback to window.onload, that will always work - window.attachEvent( "onload", jQuery.ready ); - - // If IE and not a frame - // continually check to see if the document is ready - var toplevel = false; - - try { - toplevel = window.frameElement == null; - } catch(e) {} - - if ( document.documentElement.doScroll && toplevel ) { - doScrollCheck(); - } - } - }, - - // See test/unit/core.js for details concerning isFunction. - // Since version 1.3, DOM methods and functions like alert - // aren't supported. They return false on IE (#2968). - isFunction: function( obj ) { - return jQuery.type(obj) === "function"; - }, - - isArray: Array.isArray || function( obj ) { - return jQuery.type(obj) === "array"; - }, - - isWindow: function( obj ) { - return obj != null && obj == obj.window; - }, - - isNumeric: function( obj ) { - return !isNaN( parseFloat(obj) ) && isFinite( obj ); - }, - - type: function( obj ) { - return obj == null ? - String( obj ) : - class2type[ toString.call(obj) ] || "object"; - }, - - isPlainObject: function( obj ) { - // Must be an Object. - // Because of IE, we also have to check the presence of the constructor property. - // Make sure that DOM nodes and window objects don't pass through, as well - if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { - return false; - } - - try { - // Not own constructor property must be Object - if ( obj.constructor && - !hasOwn.call(obj, "constructor") && - !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { - return false; - } - } catch ( e ) { - // IE8,9 Will throw exceptions on certain host objects #9897 - return false; - } - - // Own properties are enumerated firstly, so to speed up, - // if last one is own, then all properties are own. - - var key; - for ( key in obj ) {} - - return key === undefined || hasOwn.call( obj, key ); - }, - - isEmptyObject: function( obj ) { - for ( var name in obj ) { - return false; - } - return true; - }, - - error: function( msg ) { - throw new Error( msg ); - }, - - parseJSON: function( data ) { - if ( typeof data !== "string" || !data ) { - return null; - } - - // Make sure leading/trailing whitespace is removed (IE can't handle it) - data = jQuery.trim( data ); - - // Attempt to parse using the native JSON parser first - if ( window.JSON && window.JSON.parse ) { - return window.JSON.parse( data ); - } - - // Make sure the incoming data is actual JSON - // Logic borrowed from http://json.org/json2.js - if ( rvalidchars.test( data.replace( rvalidescape, "@" ) - .replace( rvalidtokens, "]" ) - .replace( rvalidbraces, "")) ) { - - return ( new Function( "return " + data ) )(); - - } - jQuery.error( "Invalid JSON: " + data ); - }, - - // Cross-browser xml parsing - parseXML: function( data ) { - if ( typeof data !== "string" || !data ) { - return null; - } - var xml, tmp; - try { - if ( window.DOMParser ) { // Standard - tmp = new DOMParser(); - xml = tmp.parseFromString( data , "text/xml" ); - } else { // IE - xml = new ActiveXObject( "Microsoft.XMLDOM" ); - xml.async = "false"; - xml.loadXML( data ); - } - } catch( e ) { - xml = undefined; - } - if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { - jQuery.error( "Invalid XML: " + data ); - } - return xml; - }, - - noop: function() {}, - - // Evaluates a script in a global context - // Workarounds based on findings by Jim Driscoll - // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context - globalEval: function( data ) { - if ( data && rnotwhite.test( data ) ) { - // We use execScript on Internet Explorer - // We use an anonymous function so that context is window - // rather than jQuery in Firefox - ( window.execScript || function( data ) { - window[ "eval" ].call( window, data ); - } )( data ); - } - }, - - // Convert dashed to camelCase; used by the css and data modules - // Microsoft forgot to hump their vendor prefix (#9572) - camelCase: function( string ) { - return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); - }, - - nodeName: function( elem, name ) { - return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); - }, - - // args is for internal usage only - each: function( object, callback, args ) { - var name, i = 0, - length = object.length, - isObj = length === undefined || jQuery.isFunction( object ); - - if ( args ) { - if ( isObj ) { - for ( name in object ) { - if ( callback.apply( object[ name ], args ) === false ) { - break; - } - } - } else { - for ( ; i < length; ) { - if ( callback.apply( object[ i++ ], args ) === false ) { - break; - } - } - } - - // A special, fast, case for the most common use of each - } else { - if ( isObj ) { - for ( name in object ) { - if ( callback.call( object[ name ], name, object[ name ] ) === false ) { - break; - } - } - } else { - for ( ; i < length; ) { - if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) { - break; - } - } - } - } - - return object; - }, - - // Use native String.trim function wherever possible - trim: trim ? - function( text ) { - return text == null ? - "" : - trim.call( text ); - } : - - // Otherwise use our own trimming functionality - function( text ) { - return text == null ? - "" : - text.toString().replace( trimLeft, "" ).replace( trimRight, "" ); - }, - - // results is for internal usage only - makeArray: function( array, results ) { - var ret = results || []; - - if ( array != null ) { - // The window, strings (and functions) also have 'length' - // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 - var type = jQuery.type( array ); - - if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { - push.call( ret, array ); - } else { - jQuery.merge( ret, array ); - } - } - - return ret; - }, - - inArray: function( elem, array, i ) { - var len; - - if ( array ) { - if ( indexOf ) { - return indexOf.call( array, elem, i ); - } - - len = array.length; - i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; - - for ( ; i < len; i++ ) { - // Skip accessing in sparse arrays - if ( i in array && array[ i ] === elem ) { - return i; - } - } - } - - return -1; - }, - - merge: function( first, second ) { - var i = first.length, - j = 0; - - if ( typeof second.length === "number" ) { - for ( var l = second.length; j < l; j++ ) { - first[ i++ ] = second[ j ]; - } - - } else { - while ( second[j] !== undefined ) { - first[ i++ ] = second[ j++ ]; - } - } - - first.length = i; - - return first; - }, - - grep: function( elems, callback, inv ) { - var ret = [], retVal; - inv = !!inv; - - // Go through the array, only saving the items - // that pass the validator function - for ( var i = 0, length = elems.length; i < length; i++ ) { - retVal = !!callback( elems[ i ], i ); - if ( inv !== retVal ) { - ret.push( elems[ i ] ); - } - } - - return ret; - }, - - // arg is for internal usage only - map: function( elems, callback, arg ) { - var value, key, ret = [], - i = 0, - length = elems.length, - // jquery objects are treated as arrays - isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ; - - // Go through the array, translating each of the items to their - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret[ ret.length ] = value; - } - } - - // Go through every key on the object, - } else { - for ( key in elems ) { - value = callback( elems[ key ], key, arg ); - - if ( value != null ) { - ret[ ret.length ] = value; - } - } - } - - // Flatten any nested arrays - return ret.concat.apply( [], ret ); - }, - - // A global GUID counter for objects - guid: 1, - - // Bind a function to a context, optionally partially applying any - // arguments. - proxy: function( fn, context ) { - if ( typeof context === "string" ) { - var tmp = fn[ context ]; - context = fn; - fn = tmp; - } - - // Quick check to determine if target is callable, in the spec - // this throws a TypeError, but we will just return undefined. - if ( !jQuery.isFunction( fn ) ) { - return undefined; - } - - // Simulated bind - var args = slice.call( arguments, 2 ), - proxy = function() { - return fn.apply( context, args.concat( slice.call( arguments ) ) ); - }; - - // Set the guid of unique handler to the same of original handler, so it can be removed - proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; - - return proxy; - }, - - // Mutifunctional method to get and set values to a collection - // The value/s can optionally be executed if it's a function - access: function( elems, fn, key, value, chainable, emptyGet, pass ) { - var exec, - bulk = key == null, - i = 0, - length = elems.length; - - // Sets many values - if ( key && typeof key === "object" ) { - for ( i in key ) { - jQuery.access( elems, fn, i, key[i], 1, emptyGet, value ); - } - chainable = 1; - - // Sets one value - } else if ( value !== undefined ) { - // Optionally, function values get executed if exec is true - exec = pass === undefined && jQuery.isFunction( value ); - - if ( bulk ) { - // Bulk operations only iterate when executing function values - if ( exec ) { - exec = fn; - fn = function( elem, key, value ) { - return exec.call( jQuery( elem ), value ); - }; - - // Otherwise they run against the entire set - } else { - fn.call( elems, value ); - fn = null; - } - } - - if ( fn ) { - for (; i < length; i++ ) { - fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); - } - } - - chainable = 1; - } - - return chainable ? - elems : - - // Gets - bulk ? - fn.call( elems ) : - length ? fn( elems[0], key ) : emptyGet; - }, - - now: function() { - return ( new Date() ).getTime(); - }, - - // Use of jQuery.browser is frowned upon. - // More details: http://docs.jquery.com/Utilities/jQuery.browser - uaMatch: function( ua ) { - ua = ua.toLowerCase(); - - var match = rwebkit.exec( ua ) || - ropera.exec( ua ) || - rmsie.exec( ua ) || - ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || - []; - - return { browser: match[1] || "", version: match[2] || "0" }; - }, - - sub: function() { - function jQuerySub( selector, context ) { - return new jQuerySub.fn.init( selector, context ); - } - jQuery.extend( true, jQuerySub, this ); - jQuerySub.superclass = this; - jQuerySub.fn = jQuerySub.prototype = this(); - jQuerySub.fn.constructor = jQuerySub; - jQuerySub.sub = this.sub; - jQuerySub.fn.init = function init( selector, context ) { - if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) { - context = jQuerySub( context ); - } - - return jQuery.fn.init.call( this, selector, context, rootjQuerySub ); - }; - jQuerySub.fn.init.prototype = jQuerySub.fn; - var rootjQuerySub = jQuerySub(document); - return jQuerySub; - }, - - browser: {} -}); - -// Populate the class2type map -jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); -}); - -browserMatch = jQuery.uaMatch( userAgent ); -if ( browserMatch.browser ) { - jQuery.browser[ browserMatch.browser ] = true; - jQuery.browser.version = browserMatch.version; -} - -// Deprecated, use jQuery.browser.webkit instead -if ( jQuery.browser.webkit ) { - jQuery.browser.safari = true; -} - -// IE doesn't match non-breaking spaces with \s -if ( rnotwhite.test( "\xA0" ) ) { - trimLeft = /^[\s\xA0]+/; - trimRight = /[\s\xA0]+$/; -} - -// All jQuery objects should point back to these -rootjQuery = jQuery(document); - -// Cleanup functions for the document ready method -if ( document.addEventListener ) { - DOMContentLoaded = function() { - document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); - jQuery.ready(); - }; - -} else if ( document.attachEvent ) { - DOMContentLoaded = function() { - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if ( document.readyState === "complete" ) { - document.detachEvent( "onreadystatechange", DOMContentLoaded ); - jQuery.ready(); - } - }; -} - -// The DOM ready check for Internet Explorer -function doScrollCheck() { - if ( jQuery.isReady ) { - return; - } - - try { - // If IE is used, use the trick by Diego Perini - // http://javascript.nwbox.com/IEContentLoaded/ - document.documentElement.doScroll("left"); - } catch(e) { - setTimeout( doScrollCheck, 1 ); - return; - } - - // and execute any waiting functions - jQuery.ready(); -} - -return jQuery; - -})(); - - -// String to Object flags format cache -var flagsCache = {}; - -// Convert String-formatted flags into Object-formatted ones and store in cache -function createFlags( flags ) { - var object = flagsCache[ flags ] = {}, - i, length; - flags = flags.split( /\s+/ ); - for ( i = 0, length = flags.length; i < length; i++ ) { - object[ flags[i] ] = true; - } - return object; -} - -/* - * Create a callback list using the following parameters: - * - * flags: an optional list of space-separated flags that will change how - * the callback list behaves - * - * By default a callback list will act like an event callback list and can be - * "fired" multiple times. - * - * Possible flags: - * - * once: will ensure the callback list can only be fired once (like a Deferred) - * - * memory: will keep track of previous values and will call any callback added - * after the list has been fired right away with the latest "memorized" - * values (like a Deferred) - * - * unique: will ensure a callback can only be added once (no duplicate in the list) - * - * stopOnFalse: interrupt callings when a callback returns false - * - */ -jQuery.Callbacks = function( flags ) { - - // Convert flags from String-formatted to Object-formatted - // (we check in cache first) - flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {}; - - var // Actual callback list - list = [], - // Stack of fire calls for repeatable lists - stack = [], - // Last fire value (for non-forgettable lists) - memory, - // Flag to know if list was already fired - fired, - // Flag to know if list is currently firing - firing, - // First callback to fire (used internally by add and fireWith) - firingStart, - // End of the loop when firing - firingLength, - // Index of currently firing callback (modified by remove if needed) - firingIndex, - // Add one or several callbacks to the list - add = function( args ) { - var i, - length, - elem, - type, - actual; - for ( i = 0, length = args.length; i < length; i++ ) { - elem = args[ i ]; - type = jQuery.type( elem ); - if ( type === "array" ) { - // Inspect recursively - add( elem ); - } else if ( type === "function" ) { - // Add if not in unique mode and callback is not in - if ( !flags.unique || !self.has( elem ) ) { - list.push( elem ); - } - } - } - }, - // Fire callbacks - fire = function( context, args ) { - args = args || []; - memory = !flags.memory || [ context, args ]; - fired = true; - firing = true; - firingIndex = firingStart || 0; - firingStart = 0; - firingLength = list.length; - for ( ; list && firingIndex < firingLength; firingIndex++ ) { - if ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) { - memory = true; // Mark as halted - break; - } - } - firing = false; - if ( list ) { - if ( !flags.once ) { - if ( stack && stack.length ) { - memory = stack.shift(); - self.fireWith( memory[ 0 ], memory[ 1 ] ); - } - } else if ( memory === true ) { - self.disable(); - } else { - list = []; - } - } - }, - // Actual Callbacks object - self = { - // Add a callback or a collection of callbacks to the list - add: function() { - if ( list ) { - var length = list.length; - add( arguments ); - // Do we need to add the callbacks to the - // current firing batch? - if ( firing ) { - firingLength = list.length; - // With memory, if we're not firing then - // we should call right away, unless previous - // firing was halted (stopOnFalse) - } else if ( memory && memory !== true ) { - firingStart = length; - fire( memory[ 0 ], memory[ 1 ] ); - } - } - return this; - }, - // Remove a callback from the list - remove: function() { - if ( list ) { - var args = arguments, - argIndex = 0, - argLength = args.length; - for ( ; argIndex < argLength ; argIndex++ ) { - for ( var i = 0; i < list.length; i++ ) { - if ( args[ argIndex ] === list[ i ] ) { - // Handle firingIndex and firingLength - if ( firing ) { - if ( i <= firingLength ) { - firingLength--; - if ( i <= firingIndex ) { - firingIndex--; - } - } - } - // Remove the element - list.splice( i--, 1 ); - // If we have some unicity property then - // we only need to do this once - if ( flags.unique ) { - break; - } - } - } - } - } - return this; - }, - // Control if a given callback is in the list - has: function( fn ) { - if ( list ) { - var i = 0, - length = list.length; - for ( ; i < length; i++ ) { - if ( fn === list[ i ] ) { - return true; - } - } - } - return false; - }, - // Remove all callbacks from the list - empty: function() { - list = []; - return this; - }, - // Have the list do nothing anymore - disable: function() { - list = stack = memory = undefined; - return this; - }, - // Is it disabled? - disabled: function() { - return !list; - }, - // Lock the list in its current state - lock: function() { - stack = undefined; - if ( !memory || memory === true ) { - self.disable(); - } - return this; - }, - // Is it locked? - locked: function() { - return !stack; - }, - // Call all callbacks with the given context and arguments - fireWith: function( context, args ) { - if ( stack ) { - if ( firing ) { - if ( !flags.once ) { - stack.push( [ context, args ] ); - } - } else if ( !( flags.once && memory ) ) { - fire( context, args ); - } - } - return this; - }, - // Call all the callbacks with the given arguments - fire: function() { - self.fireWith( this, arguments ); - return this; - }, - // To know if the callbacks have already been called at least once - fired: function() { - return !!fired; - } - }; - - return self; -}; - - - - -var // Static reference to slice - sliceDeferred = [].slice; - -jQuery.extend({ - - Deferred: function( func ) { - var doneList = jQuery.Callbacks( "once memory" ), - failList = jQuery.Callbacks( "once memory" ), - progressList = jQuery.Callbacks( "memory" ), - state = "pending", - lists = { - resolve: doneList, - reject: failList, - notify: progressList - }, - promise = { - done: doneList.add, - fail: failList.add, - progress: progressList.add, - - state: function() { - return state; - }, - - // Deprecated - isResolved: doneList.fired, - isRejected: failList.fired, - - then: function( doneCallbacks, failCallbacks, progressCallbacks ) { - deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks ); - return this; - }, - always: function() { - deferred.done.apply( deferred, arguments ).fail.apply( deferred, arguments ); - return this; - }, - pipe: function( fnDone, fnFail, fnProgress ) { - return jQuery.Deferred(function( newDefer ) { - jQuery.each( { - done: [ fnDone, "resolve" ], - fail: [ fnFail, "reject" ], - progress: [ fnProgress, "notify" ] - }, function( handler, data ) { - var fn = data[ 0 ], - action = data[ 1 ], - returned; - if ( jQuery.isFunction( fn ) ) { - deferred[ handler ](function() { - returned = fn.apply( this, arguments ); - if ( returned && jQuery.isFunction( returned.promise ) ) { - returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify ); - } else { - newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); - } - }); - } else { - deferred[ handler ]( newDefer[ action ] ); - } - }); - }).promise(); - }, - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function( obj ) { - if ( obj == null ) { - obj = promise; - } else { - for ( var key in promise ) { - obj[ key ] = promise[ key ]; - } - } - return obj; - } - }, - deferred = promise.promise({}), - key; - - for ( key in lists ) { - deferred[ key ] = lists[ key ].fire; - deferred[ key + "With" ] = lists[ key ].fireWith; - } - - // Handle state - deferred.done( function() { - state = "resolved"; - }, failList.disable, progressList.lock ).fail( function() { - state = "rejected"; - }, doneList.disable, progressList.lock ); - - // Call given func if any - if ( func ) { - func.call( deferred, deferred ); - } - - // All done! - return deferred; - }, - - // Deferred helper - when: function( firstParam ) { - var args = sliceDeferred.call( arguments, 0 ), - i = 0, - length = args.length, - pValues = new Array( length ), - count = length, - pCount = length, - deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ? - firstParam : - jQuery.Deferred(), - promise = deferred.promise(); - function resolveFunc( i ) { - return function( value ) { - args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; - if ( !( --count ) ) { - deferred.resolveWith( deferred, args ); - } - }; - } - function progressFunc( i ) { - return function( value ) { - pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; - deferred.notifyWith( promise, pValues ); - }; - } - if ( length > 1 ) { - for ( ; i < length; i++ ) { - if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) { - args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) ); - } else { - --count; - } - } - if ( !count ) { - deferred.resolveWith( deferred, args ); - } - } else if ( deferred !== firstParam ) { - deferred.resolveWith( deferred, length ? [ firstParam ] : [] ); - } - return promise; - } -}); - - - - -jQuery.support = (function() { - - var support, - all, - a, - select, - opt, - input, - fragment, - tds, - events, - eventName, - i, - isSupported, - div = document.createElement( "div" ), - documentElement = document.documentElement; - - // Preliminary tests - div.setAttribute("className", "t"); - div.innerHTML = "
a"; - - all = div.getElementsByTagName( "*" ); - a = div.getElementsByTagName( "a" )[ 0 ]; - - // Can't get basic test support - if ( !all || !all.length || !a ) { - return {}; - } - - // First batch of supports tests - select = document.createElement( "select" ); - opt = select.appendChild( document.createElement("option") ); - input = div.getElementsByTagName( "input" )[ 0 ]; - - support = { - // IE strips leading whitespace when .innerHTML is used - leadingWhitespace: ( div.firstChild.nodeType === 3 ), - - // Make sure that tbody elements aren't automatically inserted - // IE will insert them into empty tables - tbody: !div.getElementsByTagName("tbody").length, - - // Make sure that link elements get serialized correctly by innerHTML - // This requires a wrapper element in IE - htmlSerialize: !!div.getElementsByTagName("link").length, - - // Get the style information from getAttribute - // (IE uses .cssText instead) - style: /top/.test( a.getAttribute("style") ), - - // Make sure that URLs aren't manipulated - // (IE normalizes it by default) - hrefNormalized: ( a.getAttribute("href") === "/a" ), - - // Make sure that element opacity exists - // (IE uses filter instead) - // Use a regex to work around a WebKit issue. See #5145 - opacity: /^0.55/.test( a.style.opacity ), - - // Verify style float existence - // (IE uses styleFloat instead of cssFloat) - cssFloat: !!a.style.cssFloat, - - // Make sure that if no value is specified for a checkbox - // that it defaults to "on". - // (WebKit defaults to "" instead) - checkOn: ( input.value === "on" ), - - // Make sure that a selected-by-default option has a working selected property. - // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) - optSelected: opt.selected, - - // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) - getSetAttribute: div.className !== "t", - - // Tests for enctype support on a form(#6743) - enctype: !!document.createElement("form").enctype, - - // Makes sure cloning an html5 element does not cause problems - // Where outerHTML is undefined, this still works - html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", - - // Will be defined later - submitBubbles: true, - changeBubbles: true, - focusinBubbles: false, - deleteExpando: true, - noCloneEvent: true, - inlineBlockNeedsLayout: false, - shrinkWrapBlocks: false, - reliableMarginRight: true, - pixelMargin: true - }; - - // jQuery.boxModel DEPRECATED in 1.3, use jQuery.support.boxModel instead - jQuery.boxModel = support.boxModel = (document.compatMode === "CSS1Compat"); - - // Make sure checked status is properly cloned - input.checked = true; - support.noCloneChecked = input.cloneNode( true ).checked; - - // Make sure that the options inside disabled selects aren't marked as disabled - // (WebKit marks them as disabled) - select.disabled = true; - support.optDisabled = !opt.disabled; - - // Test to see if it's possible to delete an expando from an element - // Fails in Internet Explorer - try { - delete div.test; - } catch( e ) { - support.deleteExpando = false; - } - - if ( !div.addEventListener && div.attachEvent && div.fireEvent ) { - div.attachEvent( "onclick", function() { - // Cloning a node shouldn't copy over any - // bound event handlers (IE does this) - support.noCloneEvent = false; - }); - div.cloneNode( true ).fireEvent( "onclick" ); - } - - // Check if a radio maintains its value - // after being appended to the DOM - input = document.createElement("input"); - input.value = "t"; - input.setAttribute("type", "radio"); - support.radioValue = input.value === "t"; - - input.setAttribute("checked", "checked"); - - // #11217 - WebKit loses check when the name is after the checked attribute - input.setAttribute( "name", "t" ); - - div.appendChild( input ); - fragment = document.createDocumentFragment(); - fragment.appendChild( div.lastChild ); - - // WebKit doesn't clone checked state correctly in fragments - support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; - - // Check if a disconnected checkbox will retain its checked - // value of true after appended to the DOM (IE6/7) - support.appendChecked = input.checked; - - fragment.removeChild( input ); - fragment.appendChild( div ); - - // Technique from Juriy Zaytsev - // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/ - // We only care about the case where non-standard event systems - // are used, namely in IE. Short-circuiting here helps us to - // avoid an eval call (in setAttribute) which can cause CSP - // to go haywire. See: https://developer.mozilla.org/en/Security/CSP - if ( div.attachEvent ) { - for ( i in { - submit: 1, - change: 1, - focusin: 1 - }) { - eventName = "on" + i; - isSupported = ( eventName in div ); - if ( !isSupported ) { - div.setAttribute( eventName, "return;" ); - isSupported = ( typeof div[ eventName ] === "function" ); - } - support[ i + "Bubbles" ] = isSupported; - } - } - - fragment.removeChild( div ); - - // Null elements to avoid leaks in IE - fragment = select = opt = div = input = null; - - // Run tests that need a body at doc ready - jQuery(function() { - var container, outer, inner, table, td, offsetSupport, - marginDiv, conMarginTop, style, html, positionTopLeftWidthHeight, - paddingMarginBorderVisibility, paddingMarginBorder, - body = document.getElementsByTagName("body")[0]; - - if ( !body ) { - // Return for frameset docs that don't have a body - return; - } - - conMarginTop = 1; - paddingMarginBorder = "padding:0;margin:0;border:"; - positionTopLeftWidthHeight = "position:absolute;top:0;left:0;width:1px;height:1px;"; - paddingMarginBorderVisibility = paddingMarginBorder + "0;visibility:hidden;"; - style = "style='" + positionTopLeftWidthHeight + paddingMarginBorder + "5px solid #000;"; - html = "
" + - "" + - "
"; - - container = document.createElement("div"); - container.style.cssText = paddingMarginBorderVisibility + "width:0;height:0;position:static;top:0;margin-top:" + conMarginTop + "px"; - body.insertBefore( container, body.firstChild ); - - // Construct the test element - div = document.createElement("div"); - container.appendChild( div ); - - // Check if table cells still have offsetWidth/Height when they are set - // to display:none and there are still other visible table cells in a - // table row; if so, offsetWidth/Height are not reliable for use when - // determining if an element has been hidden directly using - // display:none (it is still safe to use offsets if a parent element is - // hidden; don safety goggles and see bug #4512 for more information). - // (only IE 8 fails this test) - div.innerHTML = "
t
"; - tds = div.getElementsByTagName( "td" ); - isSupported = ( tds[ 0 ].offsetHeight === 0 ); - - tds[ 0 ].style.display = ""; - tds[ 1 ].style.display = "none"; - - // Check if empty table cells still have offsetWidth/Height - // (IE <= 8 fail this test) - support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); - - // Check if div with explicit width and no margin-right incorrectly - // gets computed margin-right based on width of container. For more - // info see bug #3333 - // Fails in WebKit before Feb 2011 nightlies - // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right - if ( window.getComputedStyle ) { - div.innerHTML = ""; - marginDiv = document.createElement( "div" ); - marginDiv.style.width = "0"; - marginDiv.style.marginRight = "0"; - div.style.width = "2px"; - div.appendChild( marginDiv ); - support.reliableMarginRight = - ( parseInt( ( window.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0; - } - - if ( typeof div.style.zoom !== "undefined" ) { - // Check if natively block-level elements act like inline-block - // elements when setting their display to 'inline' and giving - // them layout - // (IE < 8 does this) - div.innerHTML = ""; - div.style.width = div.style.padding = "1px"; - div.style.border = 0; - div.style.overflow = "hidden"; - div.style.display = "inline"; - div.style.zoom = 1; - support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); - - // Check if elements with layout shrink-wrap their children - // (IE 6 does this) - div.style.display = "block"; - div.style.overflow = "visible"; - div.innerHTML = "
"; - support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); - } - - div.style.cssText = positionTopLeftWidthHeight + paddingMarginBorderVisibility; - div.innerHTML = html; - - outer = div.firstChild; - inner = outer.firstChild; - td = outer.nextSibling.firstChild.firstChild; - - offsetSupport = { - doesNotAddBorder: ( inner.offsetTop !== 5 ), - doesAddBorderForTableAndCells: ( td.offsetTop === 5 ) - }; - - inner.style.position = "fixed"; - inner.style.top = "20px"; - - // safari subtracts parent border width here which is 5px - offsetSupport.fixedPosition = ( inner.offsetTop === 20 || inner.offsetTop === 15 ); - inner.style.position = inner.style.top = ""; - - outer.style.overflow = "hidden"; - outer.style.position = "relative"; - - offsetSupport.subtractsBorderForOverflowNotVisible = ( inner.offsetTop === -5 ); - offsetSupport.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== conMarginTop ); - - if ( window.getComputedStyle ) { - div.style.marginTop = "1%"; - support.pixelMargin = ( window.getComputedStyle( div, null ) || { marginTop: 0 } ).marginTop !== "1%"; - } - - if ( typeof container.style.zoom !== "undefined" ) { - container.style.zoom = 1; - } - - body.removeChild( container ); - marginDiv = div = container = null; - - jQuery.extend( support, offsetSupport ); - }); - - return support; -})(); - - - - -var rbrace = /^(?:\{.*\}|\[.*\])$/, - rmultiDash = /([A-Z])/g; - -jQuery.extend({ - cache: {}, - - // Please use with caution - uuid: 0, - - // Unique for each copy of jQuery on the page - // Non-digits removed to match rinlinejQuery - expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), - - // The following elements throw uncatchable exceptions if you - // attempt to add expando properties to them. - noData: { - "embed": true, - // Ban all objects except for Flash (which handle expandos) - "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", - "applet": true - }, - - hasData: function( elem ) { - elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; - return !!elem && !isEmptyDataObject( elem ); - }, - - data: function( elem, name, data, pvt /* Internal Use Only */ ) { - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var privateCache, thisCache, ret, - internalKey = jQuery.expando, - getByName = typeof name === "string", - - // We have to handle DOM nodes and JS objects differently because IE6-7 - // can't GC object references properly across the DOM-JS boundary - isNode = elem.nodeType, - - // Only DOM nodes need the global jQuery cache; JS object data is - // attached directly to the object so GC can occur automatically - cache = isNode ? jQuery.cache : elem, - - // Only defining an ID for JS objects if its cache already exists allows - // the code to shortcut on the same path as a DOM node with no cache - id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey, - isEvents = name === "events"; - - // Avoid doing any more work than we need to when trying to get data on an - // object that has no data at all - if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) { - return; - } - - if ( !id ) { - // Only DOM nodes need a new unique ID for each element since their data - // ends up in the global cache - if ( isNode ) { - elem[ internalKey ] = id = ++jQuery.uuid; - } else { - id = internalKey; - } - } - - if ( !cache[ id ] ) { - cache[ id ] = {}; - - // Avoids exposing jQuery metadata on plain JS objects when the object - // is serialized using JSON.stringify - if ( !isNode ) { - cache[ id ].toJSON = jQuery.noop; - } - } - - // An object can be passed to jQuery.data instead of a key/value pair; this gets - // shallow copied over onto the existing cache - if ( typeof name === "object" || typeof name === "function" ) { - if ( pvt ) { - cache[ id ] = jQuery.extend( cache[ id ], name ); - } else { - cache[ id ].data = jQuery.extend( cache[ id ].data, name ); - } - } - - privateCache = thisCache = cache[ id ]; - - // jQuery data() is stored in a separate object inside the object's internal data - // cache in order to avoid key collisions between internal data and user-defined - // data. - if ( !pvt ) { - if ( !thisCache.data ) { - thisCache.data = {}; - } - - thisCache = thisCache.data; - } - - if ( data !== undefined ) { - thisCache[ jQuery.camelCase( name ) ] = data; - } - - // Users should not attempt to inspect the internal events object using jQuery.data, - // it is undocumented and subject to change. But does anyone listen? No. - if ( isEvents && !thisCache[ name ] ) { - return privateCache.events; - } - - // Check for both converted-to-camel and non-converted data property names - // If a data property was specified - if ( getByName ) { - - // First Try to find as-is property data - ret = thisCache[ name ]; - - // Test for null|undefined property data - if ( ret == null ) { - - // Try to find the camelCased property - ret = thisCache[ jQuery.camelCase( name ) ]; - } - } else { - ret = thisCache; - } - - return ret; - }, - - removeData: function( elem, name, pvt /* Internal Use Only */ ) { - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var thisCache, i, l, - - // Reference to internal data cache key - internalKey = jQuery.expando, - - isNode = elem.nodeType, - - // See jQuery.data for more information - cache = isNode ? jQuery.cache : elem, - - // See jQuery.data for more information - id = isNode ? elem[ internalKey ] : internalKey; - - // If there is already no cache entry for this object, there is no - // purpose in continuing - if ( !cache[ id ] ) { - return; - } - - if ( name ) { - - thisCache = pvt ? cache[ id ] : cache[ id ].data; - - if ( thisCache ) { - - // Support array or space separated string names for data keys - if ( !jQuery.isArray( name ) ) { - - // try the string as a key before any manipulation - if ( name in thisCache ) { - name = [ name ]; - } else { - - // split the camel cased version by spaces unless a key with the spaces exists - name = jQuery.camelCase( name ); - if ( name in thisCache ) { - name = [ name ]; - } else { - name = name.split( " " ); - } - } - } - - for ( i = 0, l = name.length; i < l; i++ ) { - delete thisCache[ name[i] ]; - } - - // If there is no data left in the cache, we want to continue - // and let the cache object itself get destroyed - if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { - return; - } - } - } - - // See jQuery.data for more information - if ( !pvt ) { - delete cache[ id ].data; - - // Don't destroy the parent cache unless the internal data object - // had been the only thing left in it - if ( !isEmptyDataObject(cache[ id ]) ) { - return; - } - } - - // Browsers that fail expando deletion also refuse to delete expandos on - // the window, but it will allow it on all other JS objects; other browsers - // don't care - // Ensure that `cache` is not a window object #10080 - if ( jQuery.support.deleteExpando || !cache.setInterval ) { - delete cache[ id ]; - } else { - cache[ id ] = null; - } - - // We destroyed the cache and need to eliminate the expando on the node to avoid - // false lookups in the cache for entries that no longer exist - if ( isNode ) { - // IE does not allow us to delete expando properties from nodes, - // nor does it have a removeAttribute function on Document nodes; - // we must handle all of these cases - if ( jQuery.support.deleteExpando ) { - delete elem[ internalKey ]; - } else if ( elem.removeAttribute ) { - elem.removeAttribute( internalKey ); - } else { - elem[ internalKey ] = null; - } - } - }, - - // For internal use only. - _data: function( elem, name, data ) { - return jQuery.data( elem, name, data, true ); - }, - - // A method for determining if a DOM node can handle the data expando - acceptData: function( elem ) { - if ( elem.nodeName ) { - var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; - - if ( match ) { - return !(match === true || elem.getAttribute("classid") !== match); - } - } - - return true; - } -}); - -jQuery.fn.extend({ - data: function( key, value ) { - var parts, part, attr, name, l, - elem = this[0], - i = 0, - data = null; - - // Gets all values - if ( key === undefined ) { - if ( this.length ) { - data = jQuery.data( elem ); - - if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { - attr = elem.attributes; - for ( l = attr.length; i < l; i++ ) { - name = attr[i].name; - - if ( name.indexOf( "data-" ) === 0 ) { - name = jQuery.camelCase( name.substring(5) ); - - dataAttr( elem, name, data[ name ] ); - } - } - jQuery._data( elem, "parsedAttrs", true ); - } - } - - return data; - } - - // Sets multiple values - if ( typeof key === "object" ) { - return this.each(function() { - jQuery.data( this, key ); - }); - } - - parts = key.split( ".", 2 ); - parts[1] = parts[1] ? "." + parts[1] : ""; - part = parts[1] + "!"; - - return jQuery.access( this, function( value ) { - - if ( value === undefined ) { - data = this.triggerHandler( "getData" + part, [ parts[0] ] ); - - // Try to fetch any internally stored data first - if ( data === undefined && elem ) { - data = jQuery.data( elem, key ); - data = dataAttr( elem, key, data ); - } - - return data === undefined && parts[1] ? - this.data( parts[0] ) : - data; - } - - parts[1] = value; - this.each(function() { - var self = jQuery( this ); - - self.triggerHandler( "setData" + part, parts ); - jQuery.data( this, key, value ); - self.triggerHandler( "changeData" + part, parts ); - }); - }, null, value, arguments.length > 1, null, false ); - }, - - removeData: function( key ) { - return this.each(function() { - jQuery.removeData( this, key ); - }); - } -}); - -function dataAttr( elem, key, data ) { - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && elem.nodeType === 1 ) { - - var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); - - data = elem.getAttribute( name ); - - if ( typeof data === "string" ) { - try { - data = data === "true" ? true : - data === "false" ? false : - data === "null" ? null : - jQuery.isNumeric( data ) ? +data : - rbrace.test( data ) ? jQuery.parseJSON( data ) : - data; - } catch( e ) {} - - // Make sure we set the data so it isn't changed later - jQuery.data( elem, key, data ); - - } else { - data = undefined; - } - } - - return data; -} - -// checks a cache object for emptiness -function isEmptyDataObject( obj ) { - for ( var name in obj ) { - - // if the public data object is empty, the private is still empty - if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { - continue; - } - if ( name !== "toJSON" ) { - return false; - } - } - - return true; -} - - - - -function handleQueueMarkDefer( elem, type, src ) { - var deferDataKey = type + "defer", - queueDataKey = type + "queue", - markDataKey = type + "mark", - defer = jQuery._data( elem, deferDataKey ); - if ( defer && - ( src === "queue" || !jQuery._data(elem, queueDataKey) ) && - ( src === "mark" || !jQuery._data(elem, markDataKey) ) ) { - // Give room for hard-coded callbacks to fire first - // and eventually mark/queue something else on the element - setTimeout( function() { - if ( !jQuery._data( elem, queueDataKey ) && - !jQuery._data( elem, markDataKey ) ) { - jQuery.removeData( elem, deferDataKey, true ); - defer.fire(); - } - }, 0 ); - } -} - -jQuery.extend({ - - _mark: function( elem, type ) { - if ( elem ) { - type = ( type || "fx" ) + "mark"; - jQuery._data( elem, type, (jQuery._data( elem, type ) || 0) + 1 ); - } - }, - - _unmark: function( force, elem, type ) { - if ( force !== true ) { - type = elem; - elem = force; - force = false; - } - if ( elem ) { - type = type || "fx"; - var key = type + "mark", - count = force ? 0 : ( (jQuery._data( elem, key ) || 1) - 1 ); - if ( count ) { - jQuery._data( elem, key, count ); - } else { - jQuery.removeData( elem, key, true ); - handleQueueMarkDefer( elem, type, "mark" ); - } - } - }, - - queue: function( elem, type, data ) { - var q; - if ( elem ) { - type = ( type || "fx" ) + "queue"; - q = jQuery._data( elem, type ); - - // Speed up dequeue by getting out quickly if this is just a lookup - if ( data ) { - if ( !q || jQuery.isArray(data) ) { - q = jQuery._data( elem, type, jQuery.makeArray(data) ); - } else { - q.push( data ); - } - } - return q || []; - } - }, - - dequeue: function( elem, type ) { - type = type || "fx"; - - var queue = jQuery.queue( elem, type ), - fn = queue.shift(), - hooks = {}; - - // If the fx queue is dequeued, always remove the progress sentinel - if ( fn === "inprogress" ) { - fn = queue.shift(); - } - - if ( fn ) { - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if ( type === "fx" ) { - queue.unshift( "inprogress" ); - } - - jQuery._data( elem, type + ".run", hooks ); - fn.call( elem, function() { - jQuery.dequeue( elem, type ); - }, hooks ); - } - - if ( !queue.length ) { - jQuery.removeData( elem, type + "queue " + type + ".run", true ); - handleQueueMarkDefer( elem, type, "queue" ); - } - } -}); - -jQuery.fn.extend({ - queue: function( type, data ) { - var setter = 2; - - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - setter--; - } - - if ( arguments.length < setter ) { - return jQuery.queue( this[0], type ); - } - - return data === undefined ? - this : - this.each(function() { - var queue = jQuery.queue( this, type, data ); - - if ( type === "fx" && queue[0] !== "inprogress" ) { - jQuery.dequeue( this, type ); - } - }); - }, - dequeue: function( type ) { - return this.each(function() { - jQuery.dequeue( this, type ); - }); - }, - // Based off of the plugin by Clint Helfers, with permission. - // http://blindsignals.com/index.php/2009/07/jquery-delay/ - delay: function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; - type = type || "fx"; - - return this.queue( type, function( next, hooks ) { - var timeout = setTimeout( next, time ); - hooks.stop = function() { - clearTimeout( timeout ); - }; - }); - }, - clearQueue: function( type ) { - return this.queue( type || "fx", [] ); - }, - // Get a promise resolved when queues of a certain type - // are emptied (fx is the type by default) - promise: function( type, object ) { - if ( typeof type !== "string" ) { - object = type; - type = undefined; - } - type = type || "fx"; - var defer = jQuery.Deferred(), - elements = this, - i = elements.length, - count = 1, - deferDataKey = type + "defer", - queueDataKey = type + "queue", - markDataKey = type + "mark", - tmp; - function resolve() { - if ( !( --count ) ) { - defer.resolveWith( elements, [ elements ] ); - } - } - while( i-- ) { - if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) || - ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) || - jQuery.data( elements[ i ], markDataKey, undefined, true ) ) && - jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) { - count++; - tmp.add( resolve ); - } - } - resolve(); - return defer.promise( object ); - } -}); - - - - -var rclass = /[\n\t\r]/g, - rspace = /\s+/, - rreturn = /\r/g, - rtype = /^(?:button|input)$/i, - rfocusable = /^(?:button|input|object|select|textarea)$/i, - rclickable = /^a(?:rea)?$/i, - rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, - getSetAttribute = jQuery.support.getSetAttribute, - nodeHook, boolHook, fixSpecified; - -jQuery.fn.extend({ - attr: function( name, value ) { - return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); - }, - - removeAttr: function( name ) { - return this.each(function() { - jQuery.removeAttr( this, name ); - }); - }, - - prop: function( name, value ) { - return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); - }, - - removeProp: function( name ) { - name = jQuery.propFix[ name ] || name; - return this.each(function() { - // try/catch handles cases where IE balks (such as removing a property on window) - try { - this[ name ] = undefined; - delete this[ name ]; - } catch( e ) {} - }); - }, - - addClass: function( value ) { - var classNames, i, l, elem, - setClass, c, cl; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).addClass( value.call(this, j, this.className) ); - }); - } - - if ( value && typeof value === "string" ) { - classNames = value.split( rspace ); - - for ( i = 0, l = this.length; i < l; i++ ) { - elem = this[ i ]; - - if ( elem.nodeType === 1 ) { - if ( !elem.className && classNames.length === 1 ) { - elem.className = value; - - } else { - setClass = " " + elem.className + " "; - - for ( c = 0, cl = classNames.length; c < cl; c++ ) { - if ( !~setClass.indexOf( " " + classNames[ c ] + " " ) ) { - setClass += classNames[ c ] + " "; - } - } - elem.className = jQuery.trim( setClass ); - } - } - } - } - - return this; - }, - - removeClass: function( value ) { - var classNames, i, l, elem, className, c, cl; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).removeClass( value.call(this, j, this.className) ); - }); - } - - if ( (value && typeof value === "string") || value === undefined ) { - classNames = ( value || "" ).split( rspace ); - - for ( i = 0, l = this.length; i < l; i++ ) { - elem = this[ i ]; - - if ( elem.nodeType === 1 && elem.className ) { - if ( value ) { - className = (" " + elem.className + " ").replace( rclass, " " ); - for ( c = 0, cl = classNames.length; c < cl; c++ ) { - className = className.replace(" " + classNames[ c ] + " ", " "); - } - elem.className = jQuery.trim( className ); - - } else { - elem.className = ""; - } - } - } - } - - return this; - }, - - toggleClass: function( value, stateVal ) { - var type = typeof value, - isBool = typeof stateVal === "boolean"; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( i ) { - jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); - }); - } - - return this.each(function() { - if ( type === "string" ) { - // toggle individual class names - var className, - i = 0, - self = jQuery( this ), - state = stateVal, - classNames = value.split( rspace ); - - while ( (className = classNames[ i++ ]) ) { - // check each className given, space seperated list - state = isBool ? state : !self.hasClass( className ); - self[ state ? "addClass" : "removeClass" ]( className ); - } - - } else if ( type === "undefined" || type === "boolean" ) { - if ( this.className ) { - // store className if set - jQuery._data( this, "__className__", this.className ); - } - - // toggle whole className - this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; - } - }); - }, - - hasClass: function( selector ) { - var className = " " + selector + " ", - i = 0, - l = this.length; - for ( ; i < l; i++ ) { - if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { - return true; - } - } - - return false; - }, - - val: function( value ) { - var hooks, ret, isFunction, - elem = this[0]; - - if ( !arguments.length ) { - if ( elem ) { - hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; - - if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { - return ret; - } - - ret = elem.value; - - return typeof ret === "string" ? - // handle most common string cases - ret.replace(rreturn, "") : - // handle cases where value is null/undef or number - ret == null ? "" : ret; - } - - return; - } - - isFunction = jQuery.isFunction( value ); - - return this.each(function( i ) { - var self = jQuery(this), val; - - if ( this.nodeType !== 1 ) { - return; - } - - if ( isFunction ) { - val = value.call( this, i, self.val() ); - } else { - val = value; - } - - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - } else if ( typeof val === "number" ) { - val += ""; - } else if ( jQuery.isArray( val ) ) { - val = jQuery.map(val, function ( value ) { - return value == null ? "" : value + ""; - }); - } - - hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; - - // If set returns undefined, fall back to normal setting - if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { - this.value = val; - } - }); - } -}); - -jQuery.extend({ - valHooks: { - option: { - get: function( elem ) { - // attributes.value is undefined in Blackberry 4.7 but - // uses .value. See #6932 - var val = elem.attributes.value; - return !val || val.specified ? elem.value : elem.text; - } - }, - select: { - get: function( elem ) { - var value, i, max, option, - index = elem.selectedIndex, - values = [], - options = elem.options, - one = elem.type === "select-one"; - - // Nothing was selected - if ( index < 0 ) { - return null; - } - - // Loop through all the selected options - i = one ? index : 0; - max = one ? index + 1 : options.length; - for ( ; i < max; i++ ) { - option = options[ i ]; - - // Don't return options that are disabled or in a disabled optgroup - if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && - (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { - - // Get the specific value for the option - value = jQuery( option ).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - // Fixes Bug #2551 -- select.val() broken in IE after form.reset() - if ( one && !values.length && options.length ) { - return jQuery( options[ index ] ).val(); - } - - return values; - }, - - set: function( elem, value ) { - var values = jQuery.makeArray( value ); - - jQuery(elem).find("option").each(function() { - this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; - }); - - if ( !values.length ) { - elem.selectedIndex = -1; - } - return values; - } - } - }, - - attrFn: { - val: true, - css: true, - html: true, - text: true, - data: true, - width: true, - height: true, - offset: true - }, - - attr: function( elem, name, value, pass ) { - var ret, hooks, notxml, - nType = elem.nodeType; - - // don't get/set attributes on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - if ( pass && name in jQuery.attrFn ) { - return jQuery( elem )[ name ]( value ); - } - - // Fallback to prop when attributes are not supported - if ( typeof elem.getAttribute === "undefined" ) { - return jQuery.prop( elem, name, value ); - } - - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - - // All attributes are lowercase - // Grab necessary hook if one is defined - if ( notxml ) { - name = name.toLowerCase(); - hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); - } - - if ( value !== undefined ) { - - if ( value === null ) { - jQuery.removeAttr( elem, name ); - return; - - } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { - return ret; - - } else { - elem.setAttribute( name, "" + value ); - return value; - } - - } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) { - return ret; - - } else { - - ret = elem.getAttribute( name ); - - // Non-existent attributes return null, we normalize to undefined - return ret === null ? - undefined : - ret; - } - }, - - removeAttr: function( elem, value ) { - var propName, attrNames, name, l, isBool, - i = 0; - - if ( value && elem.nodeType === 1 ) { - attrNames = value.toLowerCase().split( rspace ); - l = attrNames.length; - - for ( ; i < l; i++ ) { - name = attrNames[ i ]; - - if ( name ) { - propName = jQuery.propFix[ name ] || name; - isBool = rboolean.test( name ); - - // See #9699 for explanation of this approach (setting first, then removal) - // Do not do this for boolean attributes (see #10870) - if ( !isBool ) { - jQuery.attr( elem, name, "" ); - } - elem.removeAttribute( getSetAttribute ? name : propName ); - - // Set corresponding property to false for boolean attributes - if ( isBool && propName in elem ) { - elem[ propName ] = false; - } - } - } - } - }, - - attrHooks: { - type: { - set: function( elem, value ) { - // We can't allow the type property to be changed (since it causes problems in IE) - if ( rtype.test( elem.nodeName ) && elem.parentNode ) { - jQuery.error( "type property can't be changed" ); - } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { - // Setting the type on a radio button after the value resets the value in IE6-9 - // Reset value to it's default in case type is set after value - // This is for element creation - var val = elem.value; - elem.setAttribute( "type", value ); - if ( val ) { - elem.value = val; - } - return value; - } - } - }, - // Use the value property for back compat - // Use the nodeHook for button elements in IE6/7 (#1954) - value: { - get: function( elem, name ) { - if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { - return nodeHook.get( elem, name ); - } - return name in elem ? - elem.value : - null; - }, - set: function( elem, value, name ) { - if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { - return nodeHook.set( elem, value, name ); - } - // Does not return so that setAttribute is also used - elem.value = value; - } - } - }, - - propFix: { - tabindex: "tabIndex", - readonly: "readOnly", - "for": "htmlFor", - "class": "className", - maxlength: "maxLength", - cellspacing: "cellSpacing", - cellpadding: "cellPadding", - rowspan: "rowSpan", - colspan: "colSpan", - usemap: "useMap", - frameborder: "frameBorder", - contenteditable: "contentEditable" - }, - - prop: function( elem, name, value ) { - var ret, hooks, notxml, - nType = elem.nodeType; - - // don't get/set properties on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - - if ( notxml ) { - // Fix name and attach hooks - name = jQuery.propFix[ name ] || name; - hooks = jQuery.propHooks[ name ]; - } - - if ( value !== undefined ) { - if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { - return ret; - - } else { - return ( elem[ name ] = value ); - } - - } else { - if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { - return ret; - - } else { - return elem[ name ]; - } - } - }, - - propHooks: { - tabIndex: { - get: function( elem ) { - // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set - // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - var attributeNode = elem.getAttributeNode("tabindex"); - - return attributeNode && attributeNode.specified ? - parseInt( attributeNode.value, 10 ) : - rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? - 0 : - undefined; - } - } - } -}); - -// Add the tabIndex propHook to attrHooks for back-compat (different case is intentional) -jQuery.attrHooks.tabindex = jQuery.propHooks.tabIndex; - -// Hook for boolean attributes -boolHook = { - get: function( elem, name ) { - // Align boolean attributes with corresponding properties - // Fall back to attribute presence where some booleans are not supported - var attrNode, - property = jQuery.prop( elem, name ); - return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ? - name.toLowerCase() : - undefined; - }, - set: function( elem, value, name ) { - var propName; - if ( value === false ) { - // Remove boolean attributes when set to false - jQuery.removeAttr( elem, name ); - } else { - // value is true since we know at this point it's type boolean and not false - // Set boolean attributes to the same name and set the DOM property - propName = jQuery.propFix[ name ] || name; - if ( propName in elem ) { - // Only set the IDL specifically if it already exists on the element - elem[ propName ] = true; - } - - elem.setAttribute( name, name.toLowerCase() ); - } - return name; - } -}; - -// IE6/7 do not support getting/setting some attributes with get/setAttribute -if ( !getSetAttribute ) { - - fixSpecified = { - name: true, - id: true, - coords: true - }; - - // Use this for any attribute in IE6/7 - // This fixes almost every IE6/7 issue - nodeHook = jQuery.valHooks.button = { - get: function( elem, name ) { - var ret; - ret = elem.getAttributeNode( name ); - return ret && ( fixSpecified[ name ] ? ret.nodeValue !== "" : ret.specified ) ? - ret.nodeValue : - undefined; - }, - set: function( elem, value, name ) { - // Set the existing or create a new attribute node - var ret = elem.getAttributeNode( name ); - if ( !ret ) { - ret = document.createAttribute( name ); - elem.setAttributeNode( ret ); - } - return ( ret.nodeValue = value + "" ); - } - }; - - // Apply the nodeHook to tabindex - jQuery.attrHooks.tabindex.set = nodeHook.set; - - // Set width and height to auto instead of 0 on empty string( Bug #8150 ) - // This is for removals - jQuery.each([ "width", "height" ], function( i, name ) { - jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { - set: function( elem, value ) { - if ( value === "" ) { - elem.setAttribute( name, "auto" ); - return value; - } - } - }); - }); - - // Set contenteditable to false on removals(#10429) - // Setting to empty string throws an error as an invalid value - jQuery.attrHooks.contenteditable = { - get: nodeHook.get, - set: function( elem, value, name ) { - if ( value === "" ) { - value = "false"; - } - nodeHook.set( elem, value, name ); - } - }; -} - - -// Some attributes require a special call on IE -if ( !jQuery.support.hrefNormalized ) { - jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { - jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { - get: function( elem ) { - var ret = elem.getAttribute( name, 2 ); - return ret === null ? undefined : ret; - } - }); - }); -} - -if ( !jQuery.support.style ) { - jQuery.attrHooks.style = { - get: function( elem ) { - // Return undefined in the case of empty string - // Normalize to lowercase since IE uppercases css property names - return elem.style.cssText.toLowerCase() || undefined; - }, - set: function( elem, value ) { - return ( elem.style.cssText = "" + value ); - } - }; -} - -// Safari mis-reports the default selected property of an option -// Accessing the parent's selectedIndex property fixes it -if ( !jQuery.support.optSelected ) { - jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { - get: function( elem ) { - var parent = elem.parentNode; - - if ( parent ) { - parent.selectedIndex; - - // Make sure that it also works with optgroups, see #5701 - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - return null; - } - }); -} - -// IE6/7 call enctype encoding -if ( !jQuery.support.enctype ) { - jQuery.propFix.enctype = "encoding"; -} - -// Radios and checkboxes getter/setter -if ( !jQuery.support.checkOn ) { - jQuery.each([ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - get: function( elem ) { - // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified - return elem.getAttribute("value") === null ? "on" : elem.value; - } - }; - }); -} -jQuery.each([ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { - set: function( elem, value ) { - if ( jQuery.isArray( value ) ) { - return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); - } - } - }); -}); - - - - -var rformElems = /^(?:textarea|input|select)$/i, - rtypenamespace = /^([^\.]*)?(?:\.(.+))?$/, - rhoverHack = /(?:^|\s)hover(\.\S+)?\b/, - rkeyEvent = /^key/, - rmouseEvent = /^(?:mouse|contextmenu)|click/, - rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - rquickIs = /^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/, - quickParse = function( selector ) { - var quick = rquickIs.exec( selector ); - if ( quick ) { - // 0 1 2 3 - // [ _, tag, id, class ] - quick[1] = ( quick[1] || "" ).toLowerCase(); - quick[3] = quick[3] && new RegExp( "(?:^|\\s)" + quick[3] + "(?:\\s|$)" ); - } - return quick; - }, - quickIs = function( elem, m ) { - var attrs = elem.attributes || {}; - return ( - (!m[1] || elem.nodeName.toLowerCase() === m[1]) && - (!m[2] || (attrs.id || {}).value === m[2]) && - (!m[3] || m[3].test( (attrs[ "class" ] || {}).value )) - ); - }, - hoverHack = function( events ) { - return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" ); - }; - -/* - * Helper functions for managing events -- not part of the public interface. - * Props to Dean Edwards' addEvent library for many of the ideas. - */ -jQuery.event = { - - add: function( elem, types, handler, data, selector ) { - - var elemData, eventHandle, events, - t, tns, type, namespaces, handleObj, - handleObjIn, quick, handlers, special; - - // Don't attach events to noData or text/comment nodes (allow plain objects tho) - if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) { - return; - } - - // Caller can pass in an object of custom data in lieu of the handler - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - selector = handleObjIn.selector; - } - - // Make sure that the handler has a unique ID, used to find/remove it later - if ( !handler.guid ) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure and main handler, if this is the first - events = elemData.events; - if ( !events ) { - elemData.events = events = {}; - } - eventHandle = elemData.handle; - if ( !eventHandle ) { - elemData.handle = eventHandle = function( e ) { - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? - jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : - undefined; - }; - // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events - eventHandle.elem = elem; - } - - // Handle multiple events separated by a space - // jQuery(...).bind("mouseover mouseout", fn); - types = jQuery.trim( hoverHack(types) ).split( " " ); - for ( t = 0; t < types.length; t++ ) { - - tns = rtypenamespace.exec( types[t] ) || []; - type = tns[1]; - namespaces = ( tns[2] || "" ).split( "." ).sort(); - - // If event changes its type, use the special event handlers for the changed type - special = jQuery.event.special[ type ] || {}; - - // If selector defined, determine special event api type, otherwise given type - type = ( selector ? special.delegateType : special.bindType ) || type; - - // Update special based on newly reset type - special = jQuery.event.special[ type ] || {}; - - // handleObj is passed to all event handlers - handleObj = jQuery.extend({ - type: type, - origType: tns[1], - data: data, - handler: handler, - guid: handler.guid, - selector: selector, - quick: selector && quickParse( selector ), - namespace: namespaces.join(".") - }, handleObjIn ); - - // Init the event handler queue if we're the first - handlers = events[ type ]; - if ( !handlers ) { - handlers = events[ type ] = []; - handlers.delegateCount = 0; - - // Only use addEventListener/attachEvent if the special events handler returns false - if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - // Bind the global event handler to the element - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle, false ); - - } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, eventHandle ); - } - } - } - - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } - - // Add to the element's handler list, delegates in front - if ( selector ) { - handlers.splice( handlers.delegateCount++, 0, handleObj ); - } else { - handlers.push( handleObj ); - } - - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[ type ] = true; - } - - // Nullify elem to prevent memory leaks in IE - elem = null; - }, - - global: {}, - - // Detach an event or set of events from an element - remove: function( elem, types, handler, selector, mappedTypes ) { - - var elemData = jQuery.hasData( elem ) && jQuery._data( elem ), - t, tns, type, origType, namespaces, origCount, - j, events, special, handle, eventType, handleObj; - - if ( !elemData || !(events = elemData.events) ) { - return; - } - - // Once for each type.namespace in types; type may be omitted - types = jQuery.trim( hoverHack( types || "" ) ).split(" "); - for ( t = 0; t < types.length; t++ ) { - tns = rtypenamespace.exec( types[t] ) || []; - type = origType = tns[1]; - namespaces = tns[2]; - - // Unbind all events (on this namespace, if provided) for the element - if ( !type ) { - for ( type in events ) { - jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); - } - continue; - } - - special = jQuery.event.special[ type ] || {}; - type = ( selector? special.delegateType : special.bindType ) || type; - eventType = events[ type ] || []; - origCount = eventType.length; - namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.)?") + "(\\.|$)") : null; - - // Remove matching events - for ( j = 0; j < eventType.length; j++ ) { - handleObj = eventType[ j ]; - - if ( ( mappedTypes || origType === handleObj.origType ) && - ( !handler || handler.guid === handleObj.guid ) && - ( !namespaces || namespaces.test( handleObj.namespace ) ) && - ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { - eventType.splice( j--, 1 ); - - if ( handleObj.selector ) { - eventType.delegateCount--; - } - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - } - - // Remove generic event handler if we removed something and no more handlers exist - // (avoids potential for endless recursion during removal of special event handlers) - if ( eventType.length === 0 && origCount !== eventType.length ) { - if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { - jQuery.removeEvent( elem, type, elemData.handle ); - } - - delete events[ type ]; - } - } - - // Remove the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - handle = elemData.handle; - if ( handle ) { - handle.elem = null; - } - - // removeData also checks for emptiness and clears the expando if empty - // so use it instead of delete - jQuery.removeData( elem, [ "events", "handle" ], true ); - } - }, - - // Events that are safe to short-circuit if no handlers are attached. - // Native DOM events should not be added, they may have inline handlers. - customEvent: { - "getData": true, - "setData": true, - "changeData": true - }, - - trigger: function( event, data, elem, onlyHandlers ) { - // Don't do events on text and comment nodes - if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) { - return; - } - - // Event object or event type - var type = event.type || event, - namespaces = [], - cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType; - - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { - return; - } - - if ( type.indexOf( "!" ) >= 0 ) { - // Exclusive events trigger only for the exact event (no namespaces) - type = type.slice(0, -1); - exclusive = true; - } - - if ( type.indexOf( "." ) >= 0 ) { - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split("."); - type = namespaces.shift(); - namespaces.sort(); - } - - if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { - // No jQuery handlers for this event type, and it can't have inline handlers - return; - } - - // Caller can pass in an Event, Object, or just an event type string - event = typeof event === "object" ? - // jQuery.Event object - event[ jQuery.expando ] ? event : - // Object literal - new jQuery.Event( type, event ) : - // Just the event type (string) - new jQuery.Event( type ); - - event.type = type; - event.isTrigger = true; - event.exclusive = exclusive; - event.namespace = namespaces.join( "." ); - event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") : null; - ontype = type.indexOf( ":" ) < 0 ? "on" + type : ""; - - // Handle a global trigger - if ( !elem ) { - - // TODO: Stop taunting the data cache; remove global events and always attach to document - cache = jQuery.cache; - for ( i in cache ) { - if ( cache[ i ].events && cache[ i ].events[ type ] ) { - jQuery.event.trigger( event, data, cache[ i ].handle.elem, true ); - } - } - return; - } - - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } - - // Clone any incoming data and prepend the event, creating the handler arg list - data = data != null ? jQuery.makeArray( data ) : []; - data.unshift( event ); - - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( special.trigger && special.trigger.apply( elem, data ) === false ) { - return; - } - - // Determine event propagation path in advance, per W3C events spec (#9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - eventPath = [[ elem, special.bindType || type ]]; - if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { - - bubbleType = special.delegateType || type; - cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode; - old = null; - for ( ; cur; cur = cur.parentNode ) { - eventPath.push([ cur, bubbleType ]); - old = cur; - } - - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( old && old === elem.ownerDocument ) { - eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]); - } - } - - // Fire handlers on the event path - for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) { - - cur = eventPath[i][0]; - event.type = eventPath[i][1]; - - handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); - if ( handle ) { - handle.apply( cur, data ); - } - // Note that this is a bare JS function and not a jQuery handler - handle = ontype && cur[ ontype ]; - if ( handle && jQuery.acceptData( cur ) && handle.apply( cur, data ) === false ) { - event.preventDefault(); - } - } - event.type = type; - - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { - - if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && - !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { - - // Call a native DOM method on the target with the same name name as the event. - // Can't use an .isFunction() check here because IE6/7 fails that test. - // Don't do default actions on window, that's where global variables be (#6170) - // IE<9 dies on focus/blur to hidden element (#1486) - if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) { - - // Don't re-trigger an onFOO event when we call its FOO() method - old = elem[ ontype ]; - - if ( old ) { - elem[ ontype ] = null; - } - - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - elem[ type ](); - jQuery.event.triggered = undefined; - - if ( old ) { - elem[ ontype ] = old; - } - } - } - } - - return event.result; - }, - - dispatch: function( event ) { - - // Make a writable jQuery.Event from the native event object - event = jQuery.event.fix( event || window.event ); - - var handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), - delegateCount = handlers.delegateCount, - args = [].slice.call( arguments, 0 ), - run_all = !event.exclusive && !event.namespace, - special = jQuery.event.special[ event.type ] || {}, - handlerQueue = [], - i, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related; - - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[0] = event; - event.delegateTarget = this; - - // Call the preDispatch hook for the mapped type, and let it bail if desired - if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { - return; - } - - // Determine handlers that should run if there are delegated events - // Avoid non-left-click bubbling in Firefox (#3861) - if ( delegateCount && !(event.button && event.type === "click") ) { - - // Pregenerate a single jQuery object for reuse with .is() - jqcur = jQuery(this); - jqcur.context = this.ownerDocument || this; - - for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { - - // Don't process events on disabled elements (#6911, #8165) - if ( cur.disabled !== true ) { - selMatch = {}; - matches = []; - jqcur[0] = cur; - for ( i = 0; i < delegateCount; i++ ) { - handleObj = handlers[ i ]; - sel = handleObj.selector; - - if ( selMatch[ sel ] === undefined ) { - selMatch[ sel ] = ( - handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel ) - ); - } - if ( selMatch[ sel ] ) { - matches.push( handleObj ); - } - } - if ( matches.length ) { - handlerQueue.push({ elem: cur, matches: matches }); - } - } - } - } - - // Add the remaining (directly-bound) handlers - if ( handlers.length > delegateCount ) { - handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); - } - - // Run delegates first; they may want to stop propagation beneath us - for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { - matched = handlerQueue[ i ]; - event.currentTarget = matched.elem; - - for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { - handleObj = matched.matches[ j ]; - - // Triggered event must either 1) be non-exclusive and have no namespace, or - // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). - if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { - - event.data = handleObj.data; - event.handleObj = handleObj; - - ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) - .apply( matched.elem, args ); - - if ( ret !== undefined ) { - event.result = ret; - if ( ret === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - } - - // Call the postDispatch hook for the mapped type - if ( special.postDispatch ) { - special.postDispatch.call( this, event ); - } - - return event.result; - }, - - // Includes some event props shared by KeyEvent and MouseEvent - // *** attrChange attrName relatedNode srcElement are not normalized, non-W3C, deprecated, will be removed in 1.8 *** - props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), - - fixHooks: {}, - - keyHooks: { - props: "char charCode key keyCode".split(" "), - filter: function( event, original ) { - - // Add which for key events - if ( event.which == null ) { - event.which = original.charCode != null ? original.charCode : original.keyCode; - } - - return event; - } - }, - - mouseHooks: { - props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), - filter: function( event, original ) { - var eventDoc, doc, body, - button = original.button, - fromElement = original.fromElement; - - // Calculate pageX/Y if missing and clientX/Y available - if ( event.pageX == null && original.clientX != null ) { - eventDoc = event.target.ownerDocument || document; - doc = eventDoc.documentElement; - body = eventDoc.body; - - event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); - event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); - } - - // Add relatedTarget, if necessary - if ( !event.relatedTarget && fromElement ) { - event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - // Note: button is not normalized, so don't use it - if ( !event.which && button !== undefined ) { - event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); - } - - return event; - } - }, - - fix: function( event ) { - if ( event[ jQuery.expando ] ) { - return event; - } - - // Create a writable copy of the event object and normalize some properties - var i, prop, - originalEvent = event, - fixHook = jQuery.event.fixHooks[ event.type ] || {}, - copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; - - event = jQuery.Event( originalEvent ); - - for ( i = copy.length; i; ) { - prop = copy[ --i ]; - event[ prop ] = originalEvent[ prop ]; - } - - // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2) - if ( !event.target ) { - event.target = originalEvent.srcElement || document; - } - - // Target should not be a text node (#504, Safari) - if ( event.target.nodeType === 3 ) { - event.target = event.target.parentNode; - } - - // For mouse/key events; add metaKey if it's not there (#3368, IE6/7/8) - if ( event.metaKey === undefined ) { - event.metaKey = event.ctrlKey; - } - - return fixHook.filter? fixHook.filter( event, originalEvent ) : event; - }, - - special: { - ready: { - // Make sure the ready event is setup - setup: jQuery.bindReady - }, - - load: { - // Prevent triggered image.load events from bubbling to window.load - noBubble: true - }, - - focus: { - delegateType: "focusin" - }, - blur: { - delegateType: "focusout" - }, - - beforeunload: { - setup: function( data, namespaces, eventHandle ) { - // We only want to do this special case on windows - if ( jQuery.isWindow( this ) ) { - this.onbeforeunload = eventHandle; - } - }, - - teardown: function( namespaces, eventHandle ) { - if ( this.onbeforeunload === eventHandle ) { - this.onbeforeunload = null; - } - } - } - }, - - simulate: function( type, elem, event, bubble ) { - // Piggyback on a donor event to simulate a different one. - // Fake originalEvent to avoid donor's stopPropagation, but if the - // simulated event prevents default then we do the same on the donor. - var e = jQuery.extend( - new jQuery.Event(), - event, - { type: type, - isSimulated: true, - originalEvent: {} - } - ); - if ( bubble ) { - jQuery.event.trigger( e, null, elem ); - } else { - jQuery.event.dispatch.call( elem, e ); - } - if ( e.isDefaultPrevented() ) { - event.preventDefault(); - } - } -}; - -// Some plugins are using, but it's undocumented/deprecated and will be removed. -// The 1.7 special event interface should provide all the hooks needed now. -jQuery.event.handle = jQuery.event.dispatch; - -jQuery.removeEvent = document.removeEventListener ? - function( elem, type, handle ) { - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle, false ); - } - } : - function( elem, type, handle ) { - if ( elem.detachEvent ) { - elem.detachEvent( "on" + type, handle ); - } - }; - -jQuery.Event = function( src, props ) { - // Allow instantiation without the 'new' keyword - if ( !(this instanceof jQuery.Event) ) { - return new jQuery.Event( src, props ); - } - - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; - - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || - src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; - - // Event type - } else { - this.type = src; - } - - // Put explicitly provided properties onto the event object - if ( props ) { - jQuery.extend( this, props ); - } - - // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || jQuery.now(); - - // Mark it as fixed - this[ jQuery.expando ] = true; -}; - -function returnFalse() { - return false; -} -function returnTrue() { - return true; -} - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - preventDefault: function() { - this.isDefaultPrevented = returnTrue; - - var e = this.originalEvent; - if ( !e ) { - return; - } - - // if preventDefault exists run it on the original event - if ( e.preventDefault ) { - e.preventDefault(); - - // otherwise set the returnValue property of the original event to false (IE) - } else { - e.returnValue = false; - } - }, - stopPropagation: function() { - this.isPropagationStopped = returnTrue; - - var e = this.originalEvent; - if ( !e ) { - return; - } - // if stopPropagation exists run it on the original event - if ( e.stopPropagation ) { - e.stopPropagation(); - } - // otherwise set the cancelBubble property of the original event to true (IE) - e.cancelBubble = true; - }, - stopImmediatePropagation: function() { - this.isImmediatePropagationStopped = returnTrue; - this.stopPropagation(); - }, - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse -}; - -// Create mouseenter/leave events using mouseover/out and event-time checks -jQuery.each({ - mouseenter: "mouseover", - mouseleave: "mouseout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - delegateType: fix, - bindType: fix, - - handle: function( event ) { - var target = this, - related = event.relatedTarget, - handleObj = event.handleObj, - selector = handleObj.selector, - ret; - - // For mousenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || (related !== target && !jQuery.contains( target, related )) ) { - event.type = handleObj.origType; - ret = handleObj.handler.apply( this, arguments ); - event.type = fix; - } - return ret; - } - }; -}); - -// IE submit delegation -if ( !jQuery.support.submitBubbles ) { - - jQuery.event.special.submit = { - setup: function() { - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; - } - - // Lazy-add a submit handler when a descendant form may potentially be submitted - jQuery.event.add( this, "click._submit keypress._submit", function( e ) { - // Node name check avoids a VML-related crash in IE (#9807) - var elem = e.target, - form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; - if ( form && !form._submit_attached ) { - jQuery.event.add( form, "submit._submit", function( event ) { - event._submit_bubble = true; - }); - form._submit_attached = true; - } - }); - // return undefined since we don't need an event listener - }, - - postDispatch: function( event ) { - // If form was submitted by the user, bubble the event up the tree - if ( event._submit_bubble ) { - delete event._submit_bubble; - if ( this.parentNode && !event.isTrigger ) { - jQuery.event.simulate( "submit", this.parentNode, event, true ); - } - } - }, - - teardown: function() { - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; - } - - // Remove delegated handlers; cleanData eventually reaps submit handlers attached above - jQuery.event.remove( this, "._submit" ); - } - }; -} - -// IE change delegation and checkbox/radio fix -if ( !jQuery.support.changeBubbles ) { - - jQuery.event.special.change = { - - setup: function() { - - if ( rformElems.test( this.nodeName ) ) { - // IE doesn't fire change on a check/radio until blur; trigger it on click - // after a propertychange. Eat the blur-change in special.change.handle. - // This still fires onchange a second time for check/radio after blur. - if ( this.type === "checkbox" || this.type === "radio" ) { - jQuery.event.add( this, "propertychange._change", function( event ) { - if ( event.originalEvent.propertyName === "checked" ) { - this._just_changed = true; - } - }); - jQuery.event.add( this, "click._change", function( event ) { - if ( this._just_changed && !event.isTrigger ) { - this._just_changed = false; - jQuery.event.simulate( "change", this, event, true ); - } - }); - } - return false; - } - // Delegated event; lazy-add a change handler on descendant inputs - jQuery.event.add( this, "beforeactivate._change", function( e ) { - var elem = e.target; - - if ( rformElems.test( elem.nodeName ) && !elem._change_attached ) { - jQuery.event.add( elem, "change._change", function( event ) { - if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { - jQuery.event.simulate( "change", this.parentNode, event, true ); - } - }); - elem._change_attached = true; - } - }); - }, - - handle: function( event ) { - var elem = event.target; - - // Swallow native change events from checkbox/radio, we already triggered them above - if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { - return event.handleObj.handler.apply( this, arguments ); - } - }, - - teardown: function() { - jQuery.event.remove( this, "._change" ); - - return rformElems.test( this.nodeName ); - } - }; -} - -// Create "bubbling" focus and blur events -if ( !jQuery.support.focusinBubbles ) { - jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { - - // Attach a single capturing handler while someone wants focusin/focusout - var attaches = 0, - handler = function( event ) { - jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); - }; - - jQuery.event.special[ fix ] = { - setup: function() { - if ( attaches++ === 0 ) { - document.addEventListener( orig, handler, true ); - } - }, - teardown: function() { - if ( --attaches === 0 ) { - document.removeEventListener( orig, handler, true ); - } - } - }; - }); -} - -jQuery.fn.extend({ - - on: function( types, selector, data, fn, /*INTERNAL*/ one ) { - var origFn, type; - - // Types can be a map of types/handlers - if ( typeof types === "object" ) { - // ( types-Object, selector, data ) - if ( typeof selector !== "string" ) { // && selector != null - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for ( type in types ) { - this.on( type, selector, data, types[ type ], one ); - } - return this; - } - - if ( data == null && fn == null ) { - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if ( fn == null ) { - if ( typeof selector === "string" ) { - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if ( fn === false ) { - fn = returnFalse; - } else if ( !fn ) { - return this; - } - - if ( one === 1 ) { - origFn = fn; - fn = function( event ) { - // Can use an empty set, since event contains the info - jQuery().off( event ); - return origFn.apply( this, arguments ); - }; - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); - } - return this.each( function() { - jQuery.event.add( this, types, fn, data, selector ); - }); - }, - one: function( types, selector, data, fn ) { - return this.on( types, selector, data, fn, 1 ); - }, - off: function( types, selector, fn ) { - if ( types && types.preventDefault && types.handleObj ) { - // ( event ) dispatched jQuery.Event - var handleObj = types.handleObj; - jQuery( types.delegateTarget ).off( - handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, - handleObj.selector, - handleObj.handler - ); - return this; - } - if ( typeof types === "object" ) { - // ( types-object [, selector] ) - for ( var type in types ) { - this.off( type, selector, types[ type ] ); - } - return this; - } - if ( selector === false || typeof selector === "function" ) { - // ( types [, fn] ) - fn = selector; - selector = undefined; - } - if ( fn === false ) { - fn = returnFalse; - } - return this.each(function() { - jQuery.event.remove( this, types, fn, selector ); - }); - }, - - bind: function( types, data, fn ) { - return this.on( types, null, data, fn ); - }, - unbind: function( types, fn ) { - return this.off( types, null, fn ); - }, - - live: function( types, data, fn ) { - jQuery( this.context ).on( types, this.selector, data, fn ); - return this; - }, - die: function( types, fn ) { - jQuery( this.context ).off( types, this.selector || "**", fn ); - return this; - }, - - delegate: function( selector, types, data, fn ) { - return this.on( types, selector, data, fn ); - }, - undelegate: function( selector, types, fn ) { - // ( namespace ) or ( selector, types [, fn] ) - return arguments.length == 1? this.off( selector, "**" ) : this.off( types, selector, fn ); - }, - - trigger: function( type, data ) { - return this.each(function() { - jQuery.event.trigger( type, data, this ); - }); - }, - triggerHandler: function( type, data ) { - if ( this[0] ) { - return jQuery.event.trigger( type, data, this[0], true ); - } - }, - - toggle: function( fn ) { - // Save reference to arguments for access in closure - var args = arguments, - guid = fn.guid || jQuery.guid++, - i = 0, - toggler = function( event ) { - // Figure out which function to execute - var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; - jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); - - // Make sure that clicks stop - event.preventDefault(); - - // and execute the function - return args[ lastToggle ].apply( this, arguments ) || false; - }; - - // link all the functions, so any of them can unbind this click handler - toggler.guid = guid; - while ( i < args.length ) { - args[ i++ ].guid = guid; - } - - return this.click( toggler ); - }, - - hover: function( fnOver, fnOut ) { - return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); - } -}); - -jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + - "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + - "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { - - // Handle event binding - jQuery.fn[ name ] = function( data, fn ) { - if ( fn == null ) { - fn = data; - data = null; - } - - return arguments.length > 0 ? - this.on( name, null, data, fn ) : - this.trigger( name ); - }; - - if ( jQuery.attrFn ) { - jQuery.attrFn[ name ] = true; - } - - if ( rkeyEvent.test( name ) ) { - jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks; - } - - if ( rmouseEvent.test( name ) ) { - jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks; - } -}); - - - -/*! - * Sizzle CSS Selector Engine - * Copyright 2011, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * More information: http://sizzlejs.com/ - */ -(function(){ - -var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, - expando = "sizcache" + (Math.random() + '').replace('.', ''), - done = 0, - toString = Object.prototype.toString, - hasDuplicate = false, - baseHasDuplicate = true, - rBackslash = /\\/g, - rReturn = /\r\n/g, - rNonWord = /\W/; - -// Here we check if the JavaScript engine is using some sort of -// optimization where it does not always call our comparision -// function. If that is the case, discard the hasDuplicate value. -// Thus far that includes Google Chrome. -[0, 0].sort(function() { - baseHasDuplicate = false; - return 0; -}); - -var Sizzle = function( selector, context, results, seed ) { - results = results || []; - context = context || document; - - var origContext = context; - - if ( context.nodeType !== 1 && context.nodeType !== 9 ) { - return []; - } - - if ( !selector || typeof selector !== "string" ) { - return results; - } - - var m, set, checkSet, extra, ret, cur, pop, i, - prune = true, - contextXML = Sizzle.isXML( context ), - parts = [], - soFar = selector; - - // Reset the position of the chunker regexp (start from head) - do { - chunker.exec( "" ); - m = chunker.exec( soFar ); - - if ( m ) { - soFar = m[3]; - - parts.push( m[1] ); - - if ( m[2] ) { - extra = m[3]; - break; - } - } - } while ( m ); - - if ( parts.length > 1 && origPOS.exec( selector ) ) { - - if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { - set = posProcess( parts[0] + parts[1], context, seed ); - - } else { - set = Expr.relative[ parts[0] ] ? - [ context ] : - Sizzle( parts.shift(), context ); - - while ( parts.length ) { - selector = parts.shift(); - - if ( Expr.relative[ selector ] ) { - selector += parts.shift(); - } - - set = posProcess( selector, set, seed ); - } - } - - } else { - // Take a shortcut and set the context if the root selector is an ID - // (but not if it'll be faster if the inner selector is an ID) - if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && - Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { - - ret = Sizzle.find( parts.shift(), context, contextXML ); - context = ret.expr ? - Sizzle.filter( ret.expr, ret.set )[0] : - ret.set[0]; - } - - if ( context ) { - ret = seed ? - { expr: parts.pop(), set: makeArray(seed) } : - Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); - - set = ret.expr ? - Sizzle.filter( ret.expr, ret.set ) : - ret.set; - - if ( parts.length > 0 ) { - checkSet = makeArray( set ); - - } else { - prune = false; - } - - while ( parts.length ) { - cur = parts.pop(); - pop = cur; - - if ( !Expr.relative[ cur ] ) { - cur = ""; - } else { - pop = parts.pop(); - } - - if ( pop == null ) { - pop = context; - } - - Expr.relative[ cur ]( checkSet, pop, contextXML ); - } - - } else { - checkSet = parts = []; - } - } - - if ( !checkSet ) { - checkSet = set; - } - - if ( !checkSet ) { - Sizzle.error( cur || selector ); - } - - if ( toString.call(checkSet) === "[object Array]" ) { - if ( !prune ) { - results.push.apply( results, checkSet ); - - } else if ( context && context.nodeType === 1 ) { - for ( i = 0; checkSet[i] != null; i++ ) { - if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { - results.push( set[i] ); - } - } - - } else { - for ( i = 0; checkSet[i] != null; i++ ) { - if ( checkSet[i] && checkSet[i].nodeType === 1 ) { - results.push( set[i] ); - } - } - } - - } else { - makeArray( checkSet, results ); - } - - if ( extra ) { - Sizzle( extra, origContext, results, seed ); - Sizzle.uniqueSort( results ); - } - - return results; -}; - -Sizzle.uniqueSort = function( results ) { - if ( sortOrder ) { - hasDuplicate = baseHasDuplicate; - results.sort( sortOrder ); - - if ( hasDuplicate ) { - for ( var i = 1; i < results.length; i++ ) { - if ( results[i] === results[ i - 1 ] ) { - results.splice( i--, 1 ); - } - } - } - } - - return results; -}; - -Sizzle.matches = function( expr, set ) { - return Sizzle( expr, null, null, set ); -}; - -Sizzle.matchesSelector = function( node, expr ) { - return Sizzle( expr, null, null, [node] ).length > 0; -}; - -Sizzle.find = function( expr, context, isXML ) { - var set, i, len, match, type, left; - - if ( !expr ) { - return []; - } - - for ( i = 0, len = Expr.order.length; i < len; i++ ) { - type = Expr.order[i]; - - if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { - left = match[1]; - match.splice( 1, 1 ); - - if ( left.substr( left.length - 1 ) !== "\\" ) { - match[1] = (match[1] || "").replace( rBackslash, "" ); - set = Expr.find[ type ]( match, context, isXML ); - - if ( set != null ) { - expr = expr.replace( Expr.match[ type ], "" ); - break; - } - } - } - } - - if ( !set ) { - set = typeof context.getElementsByTagName !== "undefined" ? - context.getElementsByTagName( "*" ) : - []; - } - - return { set: set, expr: expr }; -}; - -Sizzle.filter = function( expr, set, inplace, not ) { - var match, anyFound, - type, found, item, filter, left, - i, pass, - old = expr, - result = [], - curLoop = set, - isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); - - while ( expr && set.length ) { - for ( type in Expr.filter ) { - if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { - filter = Expr.filter[ type ]; - left = match[1]; - - anyFound = false; - - match.splice(1,1); - - if ( left.substr( left.length - 1 ) === "\\" ) { - continue; - } - - if ( curLoop === result ) { - result = []; - } - - if ( Expr.preFilter[ type ] ) { - match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); - - if ( !match ) { - anyFound = found = true; - - } else if ( match === true ) { - continue; - } - } - - if ( match ) { - for ( i = 0; (item = curLoop[i]) != null; i++ ) { - if ( item ) { - found = filter( item, match, i, curLoop ); - pass = not ^ found; - - if ( inplace && found != null ) { - if ( pass ) { - anyFound = true; - - } else { - curLoop[i] = false; - } - - } else if ( pass ) { - result.push( item ); - anyFound = true; - } - } - } - } - - if ( found !== undefined ) { - if ( !inplace ) { - curLoop = result; - } - - expr = expr.replace( Expr.match[ type ], "" ); - - if ( !anyFound ) { - return []; - } - - break; - } - } - } - - // Improper expression - if ( expr === old ) { - if ( anyFound == null ) { - Sizzle.error( expr ); - - } else { - break; - } - } - - old = expr; - } - - return curLoop; -}; - -Sizzle.error = function( msg ) { - throw new Error( "Syntax error, unrecognized expression: " + msg ); -}; - -/** - * Utility function for retreiving the text value of an array of DOM nodes - * @param {Array|Element} elem - */ -var getText = Sizzle.getText = function( elem ) { - var i, node, - nodeType = elem.nodeType, - ret = ""; - - if ( nodeType ) { - if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { - // Use textContent || innerText for elements - if ( typeof elem.textContent === 'string' ) { - return elem.textContent; - } else if ( typeof elem.innerText === 'string' ) { - // Replace IE's carriage returns - return elem.innerText.replace( rReturn, '' ); - } else { - // Traverse it's children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling) { - ret += getText( elem ); - } - } - } else if ( nodeType === 3 || nodeType === 4 ) { - return elem.nodeValue; - } - } else { - - // If no nodeType, this is expected to be an array - for ( i = 0; (node = elem[i]); i++ ) { - // Do not traverse comment nodes - if ( node.nodeType !== 8 ) { - ret += getText( node ); - } - } - } - return ret; -}; - -var Expr = Sizzle.selectors = { - order: [ "ID", "NAME", "TAG" ], - - match: { - ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, - CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, - NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, - ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/, - TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, - CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/, - POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, - PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ - }, - - leftMatch: {}, - - attrMap: { - "class": "className", - "for": "htmlFor" - }, - - attrHandle: { - href: function( elem ) { - return elem.getAttribute( "href" ); - }, - type: function( elem ) { - return elem.getAttribute( "type" ); - } - }, - - relative: { - "+": function(checkSet, part){ - var isPartStr = typeof part === "string", - isTag = isPartStr && !rNonWord.test( part ), - isPartStrNotTag = isPartStr && !isTag; - - if ( isTag ) { - part = part.toLowerCase(); - } - - for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { - if ( (elem = checkSet[i]) ) { - while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} - - checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? - elem || false : - elem === part; - } - } - - if ( isPartStrNotTag ) { - Sizzle.filter( part, checkSet, true ); - } - }, - - ">": function( checkSet, part ) { - var elem, - isPartStr = typeof part === "string", - i = 0, - l = checkSet.length; - - if ( isPartStr && !rNonWord.test( part ) ) { - part = part.toLowerCase(); - - for ( ; i < l; i++ ) { - elem = checkSet[i]; - - if ( elem ) { - var parent = elem.parentNode; - checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; - } - } - - } else { - for ( ; i < l; i++ ) { - elem = checkSet[i]; - - if ( elem ) { - checkSet[i] = isPartStr ? - elem.parentNode : - elem.parentNode === part; - } - } - - if ( isPartStr ) { - Sizzle.filter( part, checkSet, true ); - } - } - }, - - "": function(checkSet, part, isXML){ - var nodeCheck, - doneName = done++, - checkFn = dirCheck; - - if ( typeof part === "string" && !rNonWord.test( part ) ) { - part = part.toLowerCase(); - nodeCheck = part; - checkFn = dirNodeCheck; - } - - checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); - }, - - "~": function( checkSet, part, isXML ) { - var nodeCheck, - doneName = done++, - checkFn = dirCheck; - - if ( typeof part === "string" && !rNonWord.test( part ) ) { - part = part.toLowerCase(); - nodeCheck = part; - checkFn = dirNodeCheck; - } - - checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); - } - }, - - find: { - ID: function( match, context, isXML ) { - if ( typeof context.getElementById !== "undefined" && !isXML ) { - var m = context.getElementById(match[1]); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - return m && m.parentNode ? [m] : []; - } - }, - - NAME: function( match, context ) { - if ( typeof context.getElementsByName !== "undefined" ) { - var ret = [], - results = context.getElementsByName( match[1] ); - - for ( var i = 0, l = results.length; i < l; i++ ) { - if ( results[i].getAttribute("name") === match[1] ) { - ret.push( results[i] ); - } - } - - return ret.length === 0 ? null : ret; - } - }, - - TAG: function( match, context ) { - if ( typeof context.getElementsByTagName !== "undefined" ) { - return context.getElementsByTagName( match[1] ); - } - } - }, - preFilter: { - CLASS: function( match, curLoop, inplace, result, not, isXML ) { - match = " " + match[1].replace( rBackslash, "" ) + " "; - - if ( isXML ) { - return match; - } - - for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { - if ( elem ) { - if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) { - if ( !inplace ) { - result.push( elem ); - } - - } else if ( inplace ) { - curLoop[i] = false; - } - } - } - - return false; - }, - - ID: function( match ) { - return match[1].replace( rBackslash, "" ); - }, - - TAG: function( match, curLoop ) { - return match[1].replace( rBackslash, "" ).toLowerCase(); - }, - - CHILD: function( match ) { - if ( match[1] === "nth" ) { - if ( !match[2] ) { - Sizzle.error( match[0] ); - } - - match[2] = match[2].replace(/^\+|\s*/g, ''); - - // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' - var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec( - match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || - !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); - - // calculate the numbers (first)n+(last) including if they are negative - match[2] = (test[1] + (test[2] || 1)) - 0; - match[3] = test[3] - 0; - } - else if ( match[2] ) { - Sizzle.error( match[0] ); - } - - // TODO: Move to normal caching system - match[0] = done++; - - return match; - }, - - ATTR: function( match, curLoop, inplace, result, not, isXML ) { - var name = match[1] = match[1].replace( rBackslash, "" ); - - if ( !isXML && Expr.attrMap[name] ) { - match[1] = Expr.attrMap[name]; - } - - // Handle if an un-quoted value was used - match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" ); - - if ( match[2] === "~=" ) { - match[4] = " " + match[4] + " "; - } - - return match; - }, - - PSEUDO: function( match, curLoop, inplace, result, not ) { - if ( match[1] === "not" ) { - // If we're dealing with a complex expression, or a simple one - if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { - match[3] = Sizzle(match[3], null, null, curLoop); - - } else { - var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); - - if ( !inplace ) { - result.push.apply( result, ret ); - } - - return false; - } - - } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { - return true; - } - - return match; - }, - - POS: function( match ) { - match.unshift( true ); - - return match; - } - }, - - filters: { - enabled: function( elem ) { - return elem.disabled === false && elem.type !== "hidden"; - }, - - disabled: function( elem ) { - return elem.disabled === true; - }, - - checked: function( elem ) { - return elem.checked === true; - }, - - selected: function( elem ) { - // Accessing this property makes selected-by-default - // options in Safari work properly - if ( elem.parentNode ) { - elem.parentNode.selectedIndex; - } - - return elem.selected === true; - }, - - parent: function( elem ) { - return !!elem.firstChild; - }, - - empty: function( elem ) { - return !elem.firstChild; - }, - - has: function( elem, i, match ) { - return !!Sizzle( match[3], elem ).length; - }, - - header: function( elem ) { - return (/h\d/i).test( elem.nodeName ); - }, - - text: function( elem ) { - var attr = elem.getAttribute( "type" ), type = elem.type; - // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) - // use getAttribute instead to test this case - return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null ); - }, - - radio: function( elem ) { - return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type; - }, - - checkbox: function( elem ) { - return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type; - }, - - file: function( elem ) { - return elem.nodeName.toLowerCase() === "input" && "file" === elem.type; - }, - - password: function( elem ) { - return elem.nodeName.toLowerCase() === "input" && "password" === elem.type; - }, - - submit: function( elem ) { - var name = elem.nodeName.toLowerCase(); - return (name === "input" || name === "button") && "submit" === elem.type; - }, - - image: function( elem ) { - return elem.nodeName.toLowerCase() === "input" && "image" === elem.type; - }, - - reset: function( elem ) { - var name = elem.nodeName.toLowerCase(); - return (name === "input" || name === "button") && "reset" === elem.type; - }, - - button: function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && "button" === elem.type || name === "button"; - }, - - input: function( elem ) { - return (/input|select|textarea|button/i).test( elem.nodeName ); - }, - - focus: function( elem ) { - return elem === elem.ownerDocument.activeElement; - } - }, - setFilters: { - first: function( elem, i ) { - return i === 0; - }, - - last: function( elem, i, match, array ) { - return i === array.length - 1; - }, - - even: function( elem, i ) { - return i % 2 === 0; - }, - - odd: function( elem, i ) { - return i % 2 === 1; - }, - - lt: function( elem, i, match ) { - return i < match[3] - 0; - }, - - gt: function( elem, i, match ) { - return i > match[3] - 0; - }, - - nth: function( elem, i, match ) { - return match[3] - 0 === i; - }, - - eq: function( elem, i, match ) { - return match[3] - 0 === i; - } - }, - filter: { - PSEUDO: function( elem, match, i, array ) { - var name = match[1], - filter = Expr.filters[ name ]; - - if ( filter ) { - return filter( elem, i, match, array ); - - } else if ( name === "contains" ) { - return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; - - } else if ( name === "not" ) { - var not = match[3]; - - for ( var j = 0, l = not.length; j < l; j++ ) { - if ( not[j] === elem ) { - return false; - } - } - - return true; - - } else { - Sizzle.error( name ); - } - }, - - CHILD: function( elem, match ) { - var first, last, - doneName, parent, cache, - count, diff, - type = match[1], - node = elem; - - switch ( type ) { - case "only": - case "first": - while ( (node = node.previousSibling) ) { - if ( node.nodeType === 1 ) { - return false; - } - } - - if ( type === "first" ) { - return true; - } - - node = elem; - - /* falls through */ - case "last": - while ( (node = node.nextSibling) ) { - if ( node.nodeType === 1 ) { - return false; - } - } - - return true; - - case "nth": - first = match[2]; - last = match[3]; - - if ( first === 1 && last === 0 ) { - return true; - } - - doneName = match[0]; - parent = elem.parentNode; - - if ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) { - count = 0; - - for ( node = parent.firstChild; node; node = node.nextSibling ) { - if ( node.nodeType === 1 ) { - node.nodeIndex = ++count; - } - } - - parent[ expando ] = doneName; - } - - diff = elem.nodeIndex - last; - - if ( first === 0 ) { - return diff === 0; - - } else { - return ( diff % first === 0 && diff / first >= 0 ); - } - } - }, - - ID: function( elem, match ) { - return elem.nodeType === 1 && elem.getAttribute("id") === match; - }, - - TAG: function( elem, match ) { - return (match === "*" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match; - }, - - CLASS: function( elem, match ) { - return (" " + (elem.className || elem.getAttribute("class")) + " ") - .indexOf( match ) > -1; - }, - - ATTR: function( elem, match ) { - var name = match[1], - result = Sizzle.attr ? - Sizzle.attr( elem, name ) : - Expr.attrHandle[ name ] ? - Expr.attrHandle[ name ]( elem ) : - elem[ name ] != null ? - elem[ name ] : - elem.getAttribute( name ), - value = result + "", - type = match[2], - check = match[4]; - - return result == null ? - type === "!=" : - !type && Sizzle.attr ? - result != null : - type === "=" ? - value === check : - type === "*=" ? - value.indexOf(check) >= 0 : - type === "~=" ? - (" " + value + " ").indexOf(check) >= 0 : - !check ? - value && result !== false : - type === "!=" ? - value !== check : - type === "^=" ? - value.indexOf(check) === 0 : - type === "$=" ? - value.substr(value.length - check.length) === check : - type === "|=" ? - value === check || value.substr(0, check.length + 1) === check + "-" : - false; - }, - - POS: function( elem, match, i, array ) { - var name = match[2], - filter = Expr.setFilters[ name ]; - - if ( filter ) { - return filter( elem, i, match, array ); - } - } - } -}; - -var origPOS = Expr.match.POS, - fescape = function(all, num){ - return "\\" + (num - 0 + 1); - }; - -for ( var type in Expr.match ) { - Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); - Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); -} -// Expose origPOS -// "global" as in regardless of relation to brackets/parens -Expr.match.globalPOS = origPOS; - -var makeArray = function( array, results ) { - array = Array.prototype.slice.call( array, 0 ); - - if ( results ) { - results.push.apply( results, array ); - return results; - } - - return array; -}; - -// Perform a simple check to determine if the browser is capable of -// converting a NodeList to an array using builtin methods. -// Also verifies that the returned array holds DOM nodes -// (which is not the case in the Blackberry browser) -try { - Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; - -// Provide a fallback method if it does not work -} catch( e ) { - makeArray = function( array, results ) { - var i = 0, - ret = results || []; - - if ( toString.call(array) === "[object Array]" ) { - Array.prototype.push.apply( ret, array ); - - } else { - if ( typeof array.length === "number" ) { - for ( var l = array.length; i < l; i++ ) { - ret.push( array[i] ); - } - - } else { - for ( ; array[i]; i++ ) { - ret.push( array[i] ); - } - } - } - - return ret; - }; -} - -var sortOrder, siblingCheck; - -if ( document.documentElement.compareDocumentPosition ) { - sortOrder = function( a, b ) { - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { - return a.compareDocumentPosition ? -1 : 1; - } - - return a.compareDocumentPosition(b) & 4 ? -1 : 1; - }; - -} else { - sortOrder = function( a, b ) { - // The nodes are identical, we can exit early - if ( a === b ) { - hasDuplicate = true; - return 0; - - // Fallback to using sourceIndex (in IE) if it's available on both nodes - } else if ( a.sourceIndex && b.sourceIndex ) { - return a.sourceIndex - b.sourceIndex; - } - - var al, bl, - ap = [], - bp = [], - aup = a.parentNode, - bup = b.parentNode, - cur = aup; - - // If the nodes are siblings (or identical) we can do a quick check - if ( aup === bup ) { - return siblingCheck( a, b ); - - // If no parents were found then the nodes are disconnected - } else if ( !aup ) { - return -1; - - } else if ( !bup ) { - return 1; - } - - // Otherwise they're somewhere else in the tree so we need - // to build up a full list of the parentNodes for comparison - while ( cur ) { - ap.unshift( cur ); - cur = cur.parentNode; - } - - cur = bup; - - while ( cur ) { - bp.unshift( cur ); - cur = cur.parentNode; - } - - al = ap.length; - bl = bp.length; - - // Start walking down the tree looking for a discrepancy - for ( var i = 0; i < al && i < bl; i++ ) { - if ( ap[i] !== bp[i] ) { - return siblingCheck( ap[i], bp[i] ); - } - } - - // We ended someplace up the tree so do a sibling check - return i === al ? - siblingCheck( a, bp[i], -1 ) : - siblingCheck( ap[i], b, 1 ); - }; - - siblingCheck = function( a, b, ret ) { - if ( a === b ) { - return ret; - } - - var cur = a.nextSibling; - - while ( cur ) { - if ( cur === b ) { - return -1; - } - - cur = cur.nextSibling; - } - - return 1; - }; -} - -// Check to see if the browser returns elements by name when -// querying by getElementById (and provide a workaround) -(function(){ - // We're going to inject a fake input element with a specified name - var form = document.createElement("div"), - id = "script" + (new Date()).getTime(), - root = document.documentElement; - - form.innerHTML = ""; - - // Inject it into the root element, check its status, and remove it quickly - root.insertBefore( form, root.firstChild ); - - // The workaround has to do additional checks after a getElementById - // Which slows things down for other browsers (hence the branching) - if ( document.getElementById( id ) ) { - Expr.find.ID = function( match, context, isXML ) { - if ( typeof context.getElementById !== "undefined" && !isXML ) { - var m = context.getElementById(match[1]); - - return m ? - m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? - [m] : - undefined : - []; - } - }; - - Expr.filter.ID = function( elem, match ) { - var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); - - return elem.nodeType === 1 && node && node.nodeValue === match; - }; - } - - root.removeChild( form ); - - // release memory in IE - root = form = null; -})(); - -(function(){ - // Check to see if the browser returns only elements - // when doing getElementsByTagName("*") - - // Create a fake element - var div = document.createElement("div"); - div.appendChild( document.createComment("") ); - - // Make sure no comments are found - if ( div.getElementsByTagName("*").length > 0 ) { - Expr.find.TAG = function( match, context ) { - var results = context.getElementsByTagName( match[1] ); - - // Filter out possible comments - if ( match[1] === "*" ) { - var tmp = []; - - for ( var i = 0; results[i]; i++ ) { - if ( results[i].nodeType === 1 ) { - tmp.push( results[i] ); - } - } - - results = tmp; - } - - return results; - }; - } - - // Check to see if an attribute returns normalized href attributes - div.innerHTML = ""; - - if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && - div.firstChild.getAttribute("href") !== "#" ) { - - Expr.attrHandle.href = function( elem ) { - return elem.getAttribute( "href", 2 ); - }; - } - - // release memory in IE - div = null; -})(); - -if ( document.querySelectorAll ) { - (function(){ - var oldSizzle = Sizzle, - div = document.createElement("div"), - id = "__sizzle__"; - - div.innerHTML = "

"; - - // Safari can't handle uppercase or unicode characters when - // in quirks mode. - if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { - return; - } - - Sizzle = function( query, context, extra, seed ) { - context = context || document; - - // Only use querySelectorAll on non-XML documents - // (ID selectors don't work in non-HTML documents) - if ( !seed && !Sizzle.isXML(context) ) { - // See if we find a selector to speed up - var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query ); - - if ( match && (context.nodeType === 1 || context.nodeType === 9) ) { - // Speed-up: Sizzle("TAG") - if ( match[1] ) { - return makeArray( context.getElementsByTagName( query ), extra ); - - // Speed-up: Sizzle(".CLASS") - } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) { - return makeArray( context.getElementsByClassName( match[2] ), extra ); - } - } - - if ( context.nodeType === 9 ) { - // Speed-up: Sizzle("body") - // The body element only exists once, optimize finding it - if ( query === "body" && context.body ) { - return makeArray( [ context.body ], extra ); - - // Speed-up: Sizzle("#ID") - } else if ( match && match[3] ) { - var elem = context.getElementById( match[3] ); - - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE and Opera return items - // by name instead of ID - if ( elem.id === match[3] ) { - return makeArray( [ elem ], extra ); - } - - } else { - return makeArray( [], extra ); - } - } - - try { - return makeArray( context.querySelectorAll(query), extra ); - } catch(qsaError) {} - - // qSA works strangely on Element-rooted queries - // We can work around this by specifying an extra ID on the root - // and working up from there (Thanks to Andrew Dupont for the technique) - // IE 8 doesn't work on object elements - } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { - var oldContext = context, - old = context.getAttribute( "id" ), - nid = old || id, - hasParent = context.parentNode, - relativeHierarchySelector = /^\s*[+~]/.test( query ); - - if ( !old ) { - context.setAttribute( "id", nid ); - } else { - nid = nid.replace( /'/g, "\\$&" ); - } - if ( relativeHierarchySelector && hasParent ) { - context = context.parentNode; - } - - try { - if ( !relativeHierarchySelector || hasParent ) { - return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra ); - } - - } catch(pseudoError) { - } finally { - if ( !old ) { - oldContext.removeAttribute( "id" ); - } - } - } - } - - return oldSizzle(query, context, extra, seed); - }; - - for ( var prop in oldSizzle ) { - Sizzle[ prop ] = oldSizzle[ prop ]; - } - - // release memory in IE - div = null; - })(); -} - -(function(){ - var html = document.documentElement, - matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector; - - if ( matches ) { - // Check to see if it's possible to do matchesSelector - // on a disconnected node (IE 9 fails this) - var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ), - pseudoWorks = false; - - try { - // This should fail with an exception - // Gecko does not error, returns false instead - matches.call( document.documentElement, "[test!='']:sizzle" ); - - } catch( pseudoError ) { - pseudoWorks = true; - } - - Sizzle.matchesSelector = function( node, expr ) { - // Make sure that attribute selectors are quoted - expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); - - if ( !Sizzle.isXML( node ) ) { - try { - if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { - var ret = matches.call( node, expr ); - - // IE 9's matchesSelector returns false on disconnected nodes - if ( ret || !disconnectedMatch || - // As well, disconnected nodes are said to be in a document - // fragment in IE 9, so check for that - node.document && node.document.nodeType !== 11 ) { - return ret; - } - } - } catch(e) {} - } - - return Sizzle(expr, null, null, [node]).length > 0; - }; - } -})(); - -(function(){ - var div = document.createElement("div"); - - div.innerHTML = "
"; - - // Opera can't find a second classname (in 9.6) - // Also, make sure that getElementsByClassName actually exists - if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { - return; - } - - // Safari caches class attributes, doesn't catch changes (in 3.2) - div.lastChild.className = "e"; - - if ( div.getElementsByClassName("e").length === 1 ) { - return; - } - - Expr.order.splice(1, 0, "CLASS"); - Expr.find.CLASS = function( match, context, isXML ) { - if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { - return context.getElementsByClassName(match[1]); - } - }; - - // release memory in IE - div = null; -})(); - -function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - - if ( elem ) { - var match = false; - - elem = elem[dir]; - - while ( elem ) { - if ( elem[ expando ] === doneName ) { - match = checkSet[elem.sizset]; - break; - } - - if ( elem.nodeType === 1 && !isXML ){ - elem[ expando ] = doneName; - elem.sizset = i; - } - - if ( elem.nodeName.toLowerCase() === cur ) { - match = elem; - break; - } - - elem = elem[dir]; - } - - checkSet[i] = match; - } - } -} - -function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - - if ( elem ) { - var match = false; - - elem = elem[dir]; - - while ( elem ) { - if ( elem[ expando ] === doneName ) { - match = checkSet[elem.sizset]; - break; - } - - if ( elem.nodeType === 1 ) { - if ( !isXML ) { - elem[ expando ] = doneName; - elem.sizset = i; - } - - if ( typeof cur !== "string" ) { - if ( elem === cur ) { - match = true; - break; - } - - } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { - match = elem; - break; - } - } - - elem = elem[dir]; - } - - checkSet[i] = match; - } - } -} - -if ( document.documentElement.contains ) { - Sizzle.contains = function( a, b ) { - return a !== b && (a.contains ? a.contains(b) : true); - }; - -} else if ( document.documentElement.compareDocumentPosition ) { - Sizzle.contains = function( a, b ) { - return !!(a.compareDocumentPosition(b) & 16); - }; - -} else { - Sizzle.contains = function() { - return false; - }; -} - -Sizzle.isXML = function( elem ) { - // documentElement is verified for cases where it doesn't yet exist - // (such as loading iframes in IE - #4833) - var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; - - return documentElement ? documentElement.nodeName !== "HTML" : false; -}; - -var posProcess = function( selector, context, seed ) { - var match, - tmpSet = [], - later = "", - root = context.nodeType ? [context] : context; - - // Position selectors must be done after the filter - // And so must :not(positional) so we move all PSEUDOs to the end - while ( (match = Expr.match.PSEUDO.exec( selector )) ) { - later += match[0]; - selector = selector.replace( Expr.match.PSEUDO, "" ); - } - - selector = Expr.relative[selector] ? selector + "*" : selector; - - for ( var i = 0, l = root.length; i < l; i++ ) { - Sizzle( selector, root[i], tmpSet, seed ); - } - - return Sizzle.filter( later, tmpSet ); -}; - -// EXPOSE -// Override sizzle attribute retrieval -Sizzle.attr = jQuery.attr; -Sizzle.selectors.attrMap = {}; -jQuery.find = Sizzle; -jQuery.expr = Sizzle.selectors; -jQuery.expr[":"] = jQuery.expr.filters; -jQuery.unique = Sizzle.uniqueSort; -jQuery.text = Sizzle.getText; -jQuery.isXMLDoc = Sizzle.isXML; -jQuery.contains = Sizzle.contains; - - -})(); - - -var runtil = /Until$/, - rparentsprev = /^(?:parents|prevUntil|prevAll)/, - // Note: This RegExp should be improved, or likely pulled from Sizzle - rmultiselector = /,/, - isSimple = /^.[^:#\[\.,]*$/, - slice = Array.prototype.slice, - POS = jQuery.expr.match.globalPOS, - // methods guaranteed to produce a unique set when starting from a unique set - guaranteedUnique = { - children: true, - contents: true, - next: true, - prev: true - }; - -jQuery.fn.extend({ - find: function( selector ) { - var self = this, - i, l; - - if ( typeof selector !== "string" ) { - return jQuery( selector ).filter(function() { - for ( i = 0, l = self.length; i < l; i++ ) { - if ( jQuery.contains( self[ i ], this ) ) { - return true; - } - } - }); - } - - var ret = this.pushStack( "", "find", selector ), - length, n, r; - - for ( i = 0, l = this.length; i < l; i++ ) { - length = ret.length; - jQuery.find( selector, this[i], ret ); - - if ( i > 0 ) { - // Make sure that the results are unique - for ( n = length; n < ret.length; n++ ) { - for ( r = 0; r < length; r++ ) { - if ( ret[r] === ret[n] ) { - ret.splice(n--, 1); - break; - } - } - } - } - } - - return ret; - }, - - has: function( target ) { - var targets = jQuery( target ); - return this.filter(function() { - for ( var i = 0, l = targets.length; i < l; i++ ) { - if ( jQuery.contains( this, targets[i] ) ) { - return true; - } - } - }); - }, - - not: function( selector ) { - return this.pushStack( winnow(this, selector, false), "not", selector); - }, - - filter: function( selector ) { - return this.pushStack( winnow(this, selector, true), "filter", selector ); - }, - - is: function( selector ) { - return !!selector && ( - typeof selector === "string" ? - // If this is a positional selector, check membership in the returned set - // so $("p:first").is("p:last") won't return true for a doc with two "p". - POS.test( selector ) ? - jQuery( selector, this.context ).index( this[0] ) >= 0 : - jQuery.filter( selector, this ).length > 0 : - this.filter( selector ).length > 0 ); - }, - - closest: function( selectors, context ) { - var ret = [], i, l, cur = this[0]; - - // Array (deprecated as of jQuery 1.7) - if ( jQuery.isArray( selectors ) ) { - var level = 1; - - while ( cur && cur.ownerDocument && cur !== context ) { - for ( i = 0; i < selectors.length; i++ ) { - - if ( jQuery( cur ).is( selectors[ i ] ) ) { - ret.push({ selector: selectors[ i ], elem: cur, level: level }); - } - } - - cur = cur.parentNode; - level++; - } - - return ret; - } - - // String - var pos = POS.test( selectors ) || typeof selectors !== "string" ? - jQuery( selectors, context || this.context ) : - 0; - - for ( i = 0, l = this.length; i < l; i++ ) { - cur = this[i]; - - while ( cur ) { - if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { - ret.push( cur ); - break; - - } else { - cur = cur.parentNode; - if ( !cur || !cur.ownerDocument || cur === context || cur.nodeType === 11 ) { - break; - } - } - } - } - - ret = ret.length > 1 ? jQuery.unique( ret ) : ret; - - return this.pushStack( ret, "closest", selectors ); - }, - - // Determine the position of an element within - // the matched set of elements - index: function( elem ) { - - // No argument, return index in parent - if ( !elem ) { - return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1; - } - - // index in selector - if ( typeof elem === "string" ) { - return jQuery.inArray( this[0], jQuery( elem ) ); - } - - // Locate the position of the desired element - return jQuery.inArray( - // If it receives a jQuery object, the first element is used - elem.jquery ? elem[0] : elem, this ); - }, - - add: function( selector, context ) { - var set = typeof selector === "string" ? - jQuery( selector, context ) : - jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), - all = jQuery.merge( this.get(), set ); - - return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? - all : - jQuery.unique( all ) ); - }, - - andSelf: function() { - return this.add( this.prevObject ); - } -}); - -// A painfully simple check to see if an element is disconnected -// from a document (should be improved, where feasible). -function isDisconnected( node ) { - return !node || !node.parentNode || node.parentNode.nodeType === 11; -} - -jQuery.each({ - parent: function( elem ) { - var parent = elem.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - parents: function( elem ) { - return jQuery.dir( elem, "parentNode" ); - }, - parentsUntil: function( elem, i, until ) { - return jQuery.dir( elem, "parentNode", until ); - }, - next: function( elem ) { - return jQuery.nth( elem, 2, "nextSibling" ); - }, - prev: function( elem ) { - return jQuery.nth( elem, 2, "previousSibling" ); - }, - nextAll: function( elem ) { - return jQuery.dir( elem, "nextSibling" ); - }, - prevAll: function( elem ) { - return jQuery.dir( elem, "previousSibling" ); - }, - nextUntil: function( elem, i, until ) { - return jQuery.dir( elem, "nextSibling", until ); - }, - prevUntil: function( elem, i, until ) { - return jQuery.dir( elem, "previousSibling", until ); - }, - siblings: function( elem ) { - return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); - }, - children: function( elem ) { - return jQuery.sibling( elem.firstChild ); - }, - contents: function( elem ) { - return jQuery.nodeName( elem, "iframe" ) ? - elem.contentDocument || elem.contentWindow.document : - jQuery.makeArray( elem.childNodes ); - } -}, function( name, fn ) { - jQuery.fn[ name ] = function( until, selector ) { - var ret = jQuery.map( this, fn, until ); - - if ( !runtil.test( name ) ) { - selector = until; - } - - if ( selector && typeof selector === "string" ) { - ret = jQuery.filter( selector, ret ); - } - - ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; - - if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { - ret = ret.reverse(); - } - - return this.pushStack( ret, name, slice.call( arguments ).join(",") ); - }; -}); - -jQuery.extend({ - filter: function( expr, elems, not ) { - if ( not ) { - expr = ":not(" + expr + ")"; - } - - return elems.length === 1 ? - jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : - jQuery.find.matches(expr, elems); - }, - - dir: function( elem, dir, until ) { - var matched = [], - cur = elem[ dir ]; - - while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { - if ( cur.nodeType === 1 ) { - matched.push( cur ); - } - cur = cur[dir]; - } - return matched; - }, - - nth: function( cur, result, dir, elem ) { - result = result || 1; - var num = 0; - - for ( ; cur; cur = cur[dir] ) { - if ( cur.nodeType === 1 && ++num === result ) { - break; - } - } - - return cur; - }, - - sibling: function( n, elem ) { - var r = []; - - for ( ; n; n = n.nextSibling ) { - if ( n.nodeType === 1 && n !== elem ) { - r.push( n ); - } - } - - return r; - } -}); - -// Implement the identical functionality for filter and not -function winnow( elements, qualifier, keep ) { - - // Can't pass null or undefined to indexOf in Firefox 4 - // Set to 0 to skip string check - qualifier = qualifier || 0; - - if ( jQuery.isFunction( qualifier ) ) { - return jQuery.grep(elements, function( elem, i ) { - var retVal = !!qualifier.call( elem, i, elem ); - return retVal === keep; - }); - - } else if ( qualifier.nodeType ) { - return jQuery.grep(elements, function( elem, i ) { - return ( elem === qualifier ) === keep; - }); - - } else if ( typeof qualifier === "string" ) { - var filtered = jQuery.grep(elements, function( elem ) { - return elem.nodeType === 1; - }); - - if ( isSimple.test( qualifier ) ) { - return jQuery.filter(qualifier, filtered, !keep); - } else { - qualifier = jQuery.filter( qualifier, filtered ); - } - } - - return jQuery.grep(elements, function( elem, i ) { - return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; - }); -} - - - - -function createSafeFragment( document ) { - var list = nodeNames.split( "|" ), - safeFrag = document.createDocumentFragment(); - - if ( safeFrag.createElement ) { - while ( list.length ) { - safeFrag.createElement( - list.pop() - ); - } - } - return safeFrag; -} - -var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + - "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", - rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, - rleadingWhitespace = /^\s+/, - rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, - rtagName = /<([\w:]+)/, - rtbody = /]", "i"), - // checked="checked" or checked - rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, - rscriptType = /\/(java|ecma)script/i, - rcleanScript = /^\s*", "" ], - legend: [ 1, "
", "
" ], - thead: [ 1, "", "
" ], - tr: [ 2, "", "
" ], - td: [ 3, "", "
" ], - col: [ 2, "", "
" ], - area: [ 1, "", "" ], - _default: [ 0, "", "" ] - }, - safeFragment = createSafeFragment( document ); - -wrapMap.optgroup = wrapMap.option; -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - -// IE can't serialize and - - - - - - - - -
-
-
-
- - -

Index

- -
- -
- - -
-
-
-
-
- - - - - -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_build/html/gnupg.html b/docs/_build/html/gnupg.html deleted file mode 100644 index 9606c42..0000000 --- a/docs/_build/html/gnupg.html +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - gnupg Module — python-gnupg 0.3.1 documentation - - - - - - - - - - - - - - - -
-
-
-
- -
-

gnupg Module¶

-
- - -
-
-
-
-
-

Previous topic

-

Welcome to python-gnupg’s documentation!

-

Next topic

-

setup Module

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_build/html/index.html b/docs/_build/html/index.html deleted file mode 100644 index 76cf96d..0000000 --- a/docs/_build/html/index.html +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - Welcome to python-gnupg’s documentation! — python-gnupg 0.3.1 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

Welcome to python-gnupg’s documentation!¶

-

Contents:

- -
-
-

Indices and tables¶

- -
- - -
-
-
-
-
-

Table Of Contents

- - -

Next topic

-

gnupg Module

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_build/html/objects.inv b/docs/_build/html/objects.inv deleted file mode 100644 index 3f83f00316d181310223513aa8ef6b0ac9418f95..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 210 zcmY#Z2rkIT%&Sny%qvUHE6FdaR47X=D$dN$Q!wIERtPA{&q_@$u~I0gEXl~v(@oDS zEl3B7he6a>DH!M(>lp$CLNZbnic)hEGxJhXixl$H6iPBOixtu`b5a$6baH-fK~ZXP zacYV}X>n#=xW|AIP#jVq4{ZF3p_FNmFq35~rl&6o^*}w~?AsQ>LoNe`bkviqc zGY#z@&z4VlwnVF@7tQu_b^ diff --git a/docs/_build/html/search.html b/docs/_build/html/search.html deleted file mode 100644 index e5cef7f..0000000 --- a/docs/_build/html/search.html +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - Search — python-gnupg 0.3.1 documentation - - - - - - - - - - - - - - - - - -
-
-
-
- -

Search

-
- -

- Please activate JavaScript to enable the search - functionality. -

-
-

- From here you can search these documents. Enter your search - words into the box below and click "search". Note that the search - function will automatically search for all of the words. Pages - containing fewer words won't appear in the result list. -

-
- - - -
- -
- -
- -
-
-
-
-
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_build/html/searchindex.js b/docs/_build/html/searchindex.js deleted file mode 100644 index 57d045d..0000000 --- a/docs/_build/html/searchindex.js +++ /dev/null @@ -1 +0,0 @@ -Search.setIndex({objects:{},terms:{index:0,search:0,welcom:0,python:0,modul:[0,1,2,3],indic:0,content:0,test_gnupg:[0,2],gnupg:[0,3],tabl:0,setup:[0,1],document:0,page:0},objtypes:{},titles:["Welcome to python-gnupg’s documentation!","setup Module","test_gnupg Module","gnupg Module"],objnames:{},filenames:["index","setup","test_gnupg","gnupg"]}) \ No newline at end of file diff --git a/docs/_build/html/setup.html b/docs/_build/html/setup.html deleted file mode 100644 index 1063ae6..0000000 --- a/docs/_build/html/setup.html +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - setup Module — python-gnupg 0.3.1 documentation - - - - - - - - - - - - - - - -
-
-
-
- -
-

setup Module¶

-
- - -
-
-
-
-
-

Previous topic

-

gnupg Module

-

Next topic

-

test_gnupg Module

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/_build/html/test_gnupg.html b/docs/_build/html/test_gnupg.html deleted file mode 100644 index 80bff31..0000000 --- a/docs/_build/html/test_gnupg.html +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - - - test_gnupg Module — python-gnupg 0.3.1 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

test_gnupg Module¶

-
- - -
-
-
-
-
-

Previous topic

-

setup Module

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index ef80ed2..3d7eb8e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,7 +17,13 @@ import sys, os # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('./../gnupg.py')) -#sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath('.')) + +# -- Autodoc settings ---------------------------------------------------------- +## trying to set this somewhere... +autodoc_member_order = 'bysource' +autodoc_default_flags = ['members', 'show-inheritance', 'undoc-members'] +autoclass_content = 'both' # -- General configuration ----------------------------------------------------- From e8a536d4b148e62207d3939d3940c6be28d353a0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 5 Apr 2013 23:11:35 +0000 Subject: [PATCH 009/397] Update top-level Makefile with documentation generation commands. --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1eb8024..de9b6b7 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,10 @@ test: cleantest install: python setup.py install -docs: +clean-docs: sphinx-apidoc -o docs -F -A "Isis Agora Lovecruft" -H "python-gnupg" -V 0.3.1 -R 0.3.1 . + +docs: cd docs + make clean make html From 7d22ddeabc20e87b2ed6e55d6932422a9e1f2949 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 5 Apr 2013 23:12:53 +0000 Subject: [PATCH 010/397] Add uninstall command to top-level Makefile. --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index de9b6b7..ab43041 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,10 @@ test: cleantest python test_gnupg.py basic install: - python setup.py install + python setup.py install --record installed-files.txt + +uninstall: + cat installed-files.txt | sudo xargs rm -rf clean-docs: sphinx-apidoc -o docs -F -A "Isis Agora Lovecruft" -H "python-gnupg" -V 0.3.1 -R 0.3.1 . From 48ce05db1f0d0b1a9e51a55f13989bd4bb3b22d5 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 5 Apr 2013 23:20:39 +0000 Subject: [PATCH 011/397] Update README to use markdown and include better instructions. --- README | 12 ------------ README.md | 33 +++++++++++++++++++++++++++++++++ TODO.md | 1 + 3 files changed, 34 insertions(+), 12 deletions(-) delete mode 100644 README create mode 100644 README.md create mode 100644 TODO.md diff --git a/README b/README deleted file mode 100644 index b553aa2..0000000 --- a/README +++ /dev/null @@ -1,12 +0,0 @@ -python-gnupg -============ - -Fork of python-gnupg-0.3.2, patched to remove Popen([...], shell=True). - -Installation ------------- -To install this package from a source distribution, do the following. - -1. Extract all the files in the distribution archive to some directory on your system. -2. In that directory, run "python setup.py install". -3. Optionally, run "python test_gnupg.py" to ensure that the package is working as expected. diff --git a/README.md b/README.md new file mode 100644 index 0000000..aa46ee7 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# python-gnupg # +================ + +Fork of python-gnupg-0.3.2, patched to remove Popen([...], shell=True). + +### Installation ### +-------------------- +To install this package from this git repository, do: + +``` +git clone https://github.com/isislovecruft/python-gnupg.git +cd python-gnupg +make install +make test +``` + +Optionally to build the documentation after installation, do: +``` +make docs +``` + +To get started using python-gnupg's API, see the documentation online at [XXX +FIXME add readthedocs link](), and import the module like so: +``` +>>> import gnupg +``` + + +To install this package from a tarballed source distribution, do the following: + +1. Extract all the files in the distribution archive to some directory on your system. +2. In that directory, run "python setup.py install". +3. Optionally, run "python test_gnupg.py" to ensure that the package is working as expected. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..3ea950b --- /dev/null +++ b/TODO.md @@ -0,0 +1 @@ +- [ ] Separate into separate files, having one module file with everything and nearly 2000 LOC is a little bit unwieldy. From 48a4055db090f1d0a72ff9963213e77a9f8edf23 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 5 Apr 2013 23:25:10 +0000 Subject: [PATCH 012/397] Add distutils record of installation files to .gitignore. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index c2b65d1..f6a314e 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ TAGS # Ignore log files and directories from tests: keys/* *.log + +# Ignore distutils record of installed files: +installed-files.txt From f9bd8e928ea93fa6945f26a808702376bc6d3aaf Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 6 Apr 2013 00:34:13 +0000 Subject: [PATCH 013/397] Updated documentation and .rst files for Sphinx. --- docs/_static/haiku.css | 386 +++++++++++++++++++++++++++++++++++++++++ docs/conf.py | 24 ++- docs/gnupg.rst | 6 + docs/index.rst | 6 +- docs/setup.rst | 7 - docs/test_gnupg.rst | 7 - gnupg.py | 165 ++++++++++-------- 7 files changed, 503 insertions(+), 98 deletions(-) create mode 100644 docs/_static/haiku.css delete mode 100644 docs/setup.rst delete mode 100644 docs/test_gnupg.rst diff --git a/docs/_static/haiku.css b/docs/_static/haiku.css new file mode 100644 index 0000000..8e28149 --- /dev/null +++ b/docs/_static/haiku.css @@ -0,0 +1,386 @@ +/* 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; +} diff --git a/docs/conf.py b/docs/conf.py index 3d7eb8e..6f1b2a1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,9 +55,10 @@ copyright = u'2013, Isis Agora Lovecruft' # built documents. # # The short X.Y version. -version = '0.3.1' +from gnupg import __version__ +version = __version__ # The full version, including alpha/beta/rc tags. -release = '0.3.1' +release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -99,6 +100,19 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' +html_theme = 'scrolls' +html_theme = 'traditional' +html_theme = 'nature' +html_theme = 'pyramid' +html_theme = 'agogo' +html_theme = 'haiku' +html_theme_options = { +# 'stickysidebar': 'true', +# 'rightsidebar':'true', + 'nosidebar': 'false', +# 'full_logo': 'false' + 'sidebarwidth': '300' + } # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -135,7 +149,7 @@ html_static_path = ['_static'] # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} @@ -154,13 +168,13 @@ html_static_path = ['_static'] #html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +html_show_copyright = False # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the diff --git a/docs/gnupg.rst b/docs/gnupg.rst index 0b159f9..de78fdb 100644 --- a/docs/gnupg.rst +++ b/docs/gnupg.rst @@ -5,3 +5,9 @@ gnupg Module :members: :undoc-members: :show-inheritance: + + +GPG +--- + +.. autoclass:: gnupg.GPG diff --git a/docs/index.rst b/docs/index.rst index d2b196d..90ab0a2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,8 +3,8 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to python-gnupg's documentation! -======================================== +python-gnupg documentation +========================== Contents: @@ -12,8 +12,6 @@ Contents: :maxdepth: 4 gnupg - setup - test_gnupg Indices and tables diff --git a/docs/setup.rst b/docs/setup.rst deleted file mode 100644 index e3c5b3d..0000000 --- a/docs/setup.rst +++ /dev/null @@ -1,7 +0,0 @@ -setup Module -============ - -.. automodule:: setup - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/test_gnupg.rst b/docs/test_gnupg.rst deleted file mode 100644 index 40de5ba..0000000 --- a/docs/test_gnupg.rst +++ /dev/null @@ -1,7 +0,0 @@ -test_gnupg Module -================= - -.. automodule:: test_gnupg - :members: - :undoc-members: - :show-inheritance: diff --git a/gnupg.py b/gnupg.py index 379da08..01b65e3 100644 --- a/gnupg.py +++ b/gnupg.py @@ -14,13 +14,15 @@ This version is patched to exclude calls to :class:`subprocess.Popen([...], shell=True)`, and it also attempts to provide sanitization of arguments presented to gnupg, in order to avoid potential vulnerabilities. -@authors: A.M. Kuchling - Steve Traugott - Vinay Sajip - Isis Lovecruft, 0x2cdb8b35 +:Info: see +:Authors: A.M. Kuchling, Steve Traugott, Vinay Sajip, Isis Lovecruft +:Date: $Date: 2013-04-04 01:11:01 +0000 (Thursday, April 4, 2013) $ +:Description: Documentation of python-gnupg, a Python module for GnuPG. + +Previous Authors' Documentation: +-------------------------------- Steve Traugott's documentation: -------------------------------- Portions of this module are derived from A.M. Kuchling's well-designed GPG.py, using Richard Jones' updated version 1.3, which can be found in the pycrypto CVS repository on Sourceforge: @@ -41,7 +43,6 @@ Steve Traugott's documentation: Thu Jun 23 21:27:20 PDT 2005 Vinay Sajip's documentation: ----------------------------- This version of the module has been modified from Steve Traugott's version (see http://trac.t7a.org/isconf/browser/trunk/lib/python/isconf/GPG.py) by Vinay Sajip to make use of the subprocess module (Steve's version uses @@ -51,6 +52,7 @@ Vinay Sajip's documentation: A unittest harness (test_gnupg.py) has also been added. Modifications Copyright (C) 2008-2012 Vinay Sajip. All rights reserved. + """ __module__ = 'gnupg' @@ -102,15 +104,15 @@ class ProtectedOption(Exception): """Raised when the option passed to GPG is disallowed.""" class UsageError(Exception): - """Raised when you're Doing It Wrong.""" + """Raised when incorrect usage of the API occurs..""" def _copy_data(instream, outstream): """ Copy data from one stream to another. - @param instream: A file descriptor to read from. - @param outstream: A file descriptor to write to. + :param instream: A file descriptor to read from. + :param outstream: A file descriptor to write to. """ sent = 0 @@ -157,7 +159,7 @@ def _fix_unsafe(input): Find characters used to escape from a string into a shell, and wrap them in quotes if they exist. Regex pilfered from python-3.x shlex module. - @param input: The input intended for the gnupg process. + :param input: The input intended for the gnupg process. """ ## xxx do we want to add ';'? _unsafe = re.compile(r'[^\w@%+=:,./-]', 256) @@ -175,11 +177,11 @@ def _has_readwrite(path): Determine if the real uid/gid of the executing user has read and write permissions for a directory or a file. - @type path: C{str} - @param path: The path to the directory or file to check permissions for. + :type path: C{str} + :param path: The path to the directory or file to check permissions for. - @rtype: C{bool} - @param: True if real uid/gid has read+write permissions, False otherwise. + :rtype: C{bool} + :param: True if real uid/gid has read+write permissions, False otherwise. """ return os.access(path, os.R_OK and os.W_OK) @@ -188,14 +190,14 @@ def _hyphenate(input, add_prefix=False): Change underscores to hyphens so that object attributes can be easily tranlated to GPG option names. - @type input: C{str} - @param input: The attribute to hyphenate. + :type input: C{str} + :param input: The attribute to hyphenate. - @type add_prefix: C{bool} - @param add_prefix: If True, add leading hyphens to the input. + :type add_prefix: C{bool} + :param add_prefix: If True, add leading hyphens to the input. - @rtype: C{str} - @return: The :param:input with underscores changed to hyphens. + :rtype: C{str} + :return: The :param:input with underscores changed to hyphens. """ ret = '--' if add_prefix else '' ret += input.replace('_', '-') @@ -207,25 +209,25 @@ def _is_allowed(input): options, the latter being a strict subset of the set of all options known to GPG. - @type input: C{str} - @param input: An input meant to be parsed as an option or flag to the GnuPG + :type input: C{str} + :param input: An input meant to be parsed as an option or flag to the GnuPG process. Should be formatted the same as an option or flag to the commandline gpg, i.e. "--encrypt-files". - @type _possible: C{frozenset} - @ivar _possible: All known GPG options and flags. + :type _possible: C{frozenset} + :ivar _possible: All known GPG options and flags. - @type _allowed: C{frozenset} - @ivar _allowed: All allowed GPG options and flags, e.g. all GPG options and + :type _allowed: C{frozenset} + :ivar _allowed: All allowed GPG options and flags, e.g. all GPG options and flags which we are willing to acknowledge and parse. If we want to support a new option, it will need to have its own parsing class and its name will need to be added to this set. - @rtype: C{Exception} or C{str} - @raise: UsageError if :ivar:_allowed is not a subset of :ivar:_possible. + :rtype: C{Exception} or C{str} + :raise: UsageError if :ivar:_allowed is not a subset of :ivar:_possible. ProtectedOption if :param:input is not in the set :ivar:_allowed. - @return: The original parameter :param:input, unmodified and unsanitized, + :return: The original parameter :param:input, unmodified and unsanitized, if no errors occur. """ @@ -498,11 +500,11 @@ def _sanitise(*args): If you're asking, "Is this *really* necessary?": No. Not really. See: https://xkcd.com/1181/ - @type args: C{str} - @param args: (optional) The boolean arguments which will be passed to the + :type args: C{str} + :param args: (optional) The boolean arguments which will be passed to the GnuPG process. - @rtype: C{str} - @param: :ivar:sanitised + :rtype: C{str} + :param: :ivar:sanitised """ def _check_arg_and_value(arg, value): @@ -511,17 +513,17 @@ def _sanitise(*args): quote out any escape characters in :param:values, and add the pair to :ivar:sanitised. - @type arg: C{str} + :type arg: C{str} - @param arg: The arguments which will be passed to the GnuPG process, + :param arg: The arguments which will be passed to the GnuPG process, and, optionally their corresponding values. The values are any additional arguments following the GnuPG option or flag. For example, if we wanted to pass "--encrypt --recipient isis@leap.se" to gpg, then "--encrypt" would be an arg without a value, and "--recipient" would also be an arg, with a value of "isis@leap.se". - @type sanitised: C{str} - @ivar sanitised: The sanitised, allowed options. + :type sanitised: C{str} + :ivar sanitised: The sanitised, allowed options. """ safe_values = str() @@ -604,10 +606,10 @@ def _sanitise_list(arg_list): """ A generator for running through a list of gpg options and sanitising them. - @type arg_list: C{list} - @param arg_list: A list of options and flags for gpg. - @rtype: C{generator} - @return: A generator whose next() method returns each of the items in + :type arg_list: C{list} + :param arg_list: A list of options and flags for gpg. + :rtype: C{generator} + :return: A generator whose next() method returns each of the items in :param:arg_list after calling :func:_sanitise with that item as a parameter. """ @@ -629,14 +631,14 @@ def _underscore(input, remove_prefix=False): Change hyphens to underscores so that GPG option names can be easily tranlated to object attributes. - @type input: C{str} - @param input: The input intended for the gnupg process. + :type input: C{str} + :param input: The input intended for the gnupg process. - @type remove_prefix: C{bool} - @param remove_prefix: If True, strip leading hyphens from the input. + :type remove_prefix: C{bool} + :param remove_prefix: If True, strip leading hyphens from the input. - @rtype: C{str} - @return: The :param:input with hyphens changed to underscores. + :rtype: C{str} + :return: The :param:input with hyphens changed to underscores. """ if not remove_prefix: return input.replace('-', '_') @@ -660,12 +662,12 @@ def _which(executable, flags=os.X_OK): manipulate the environment's PATH settings from placing malicious code higher in the PATH. It also does happily follows links. - @type name: C{str} - @param name: The name for which to search. - @type flags: C{int} - @param flags: Arguments to L{os.access}. - @rtype: C{list} - @param: A list of the full paths to files found, in the order in which + :type name: C{str} + :param name: The name for which to search. + :type flags: C{int} + :param flags: Arguments to L{os.access}. + :rtype: C{list} + :param: A list of the full paths to files found, in the order in which they were found. """ result = [] @@ -1064,37 +1066,37 @@ class GPG(object): """ Initialize a GnuPG process wrapper. - @type gpgbinary: C{str} - @param gpgbinary: Name for GnuPG binary executable. If the absolute + :type gpgbinary: C{str} + :param gpgbinary: Name for GnuPG binary executable. If the absolute path is not given, the evironment variable $PATH is searched for the executable and checked that the real uid/gid of the user has sufficient permissions. - @type gpghome: C{str} - @param gpghome: Full pathname to directory containing the public and + :type gpghome: C{str} + :param gpghome: Full pathname to directory containing the public and private keyrings. Default is whatever GnuPG defaults to. - @type keyring: C{str} - @param keyring: raises C{DeprecationWarning}. Use :param:secring. + :type keyring: C{str} + :param keyring: raises C{DeprecationWarning}. Use :param:secring. - @type secring: C{str} - @param secring: Name of alternative secret keyring file to use. If left + :type secring: C{str} + :param secring: Name of alternative secret keyring file to use. If left unspecified, this will default to using 'secring.gpg' in the :param:gpghome directory, and create that file if it does not exist. - @type pubring: C{str} - @param pubring: Name of alternative public keyring file to use. If left + :type pubring: C{str} + :param pubring: Name of alternative public keyring file to use. If left unspecified, this will default to using 'pubring.gpg' in the :param:gpghome directory, and create that file if it does not exist. - @options: A list of additional options to pass to the GPG binary. + :options: A list of additional options to pass to the GPG binary. - @rtype: C{Exception} or C{} - @raises: RuntimeError with explanation message if there is a problem + :rtype: C{Exception} or C{} + :raises: RuntimeError with explanation message if there is a problem invoking gpg. - @returns: + :returns: """ if not gpghome: @@ -1389,13 +1391,13 @@ class GPG(object): Verify the signature on the contents of a file or file-like object. Can handle embedded signatures as well as detached signatures. If using detached signatures, the file containing the - detached signature should be specified as the :param:`data_filename`. + detached signature should be specified as the :param:data_filename. - @param file: A file descriptor object. Its type will be checked with - :func:`_is_file`. - @param data_filename: (optional) A file containing the GPG signature - data for :param:`file`. If given, :param:`file` - is verified via this detached signature. + :param file: A file descriptor object. Its type will be checked with + :func:_is_file. + :param data_filename: A file containing the GPG signature data for + :param:file. If given, :param:file is verified + via this detached signature. """ ## attempt to wrap any escape characters in quotes: safe_file = _fix_unsafe(file) @@ -1667,7 +1669,7 @@ class GPG(object): def encrypt_file(self, file, recipients, sign=None, always_trust=False, passphrase=None, armor=True, output=None, symmetric=False): - """Encrypt the message read from the file-like object 'file'""" + """Encrypt the message read from the file-like object :param:file .""" args = ['--encrypt'] if symmetric: args = ['--symmetric'] @@ -1693,7 +1695,7 @@ class GPG(object): return result def encrypt(self, data, recipients, **kwargs): - """Encrypt the message contained in the string 'data' + """Encrypt the message contained in the string :param:data . >>> import shutil >>> if os.path.exists("keys"): @@ -1739,6 +1741,11 @@ class GPG(object): return result def decrypt(self, message, **kwargs): + """ + Decrypt the contents of a string or file-like object :param:message . + + :param message: A string or file-like object to decrypt. + """ data = _make_binary_stream(message, self.encoding) result = self.decrypt_file(data, **kwargs) data.close() @@ -1746,6 +1753,14 @@ class GPG(object): def decrypt_file(self, file, always_trust=False, passphrase=None, output=None): + """ + Decrypt the contents of a file-like object :param:file . + + :param file: A file-like object to decrypt. + :param always_trust: Instruct GnuPG to ignore trust checks. + :param passphrase: The passphrase for the secret key used for decryption. + :param output: A file to write the decrypted output to. + """ args = ["--decrypt"] if output: # write the output to a file with the specified name if os.path.exists(output): From 24881a2e9e238592d44c0d057f12143477b3c6af Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 9 Apr 2013 17:32:09 +0000 Subject: [PATCH 014/397] Add better unittests for gnupg.GPG.gen_key() and gen_key_input(). * Add unittest suite for genkey tests. --- test_gnupg.py | 114 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 88 insertions(+), 26 deletions(-) diff --git a/test_gnupg.py b/test_gnupg.py index 6be4854..debe0cc 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -192,30 +192,81 @@ class GPGTestCase(unittest.TestCase): instream = io.BytesIO("This is a string of bytes mapped in memory.") outstream = str("And this one is just a string.") + def expire_today(self): + """Make an expiry date set for today""" + from datetime import datetime + today = datetime.today() + date = "%s" % today.date() + return date - def generate_key(self, first_name, last_name, domain, passphrase=None): - """Generate a key""" + def generate_key_input(self, real_name, email_domain, key_length=None, + key_type=None, subkey_type=None, passphrase=None): + """Generate a GnuPG batch file for key unattended key creation""" + name = real_name.lower().replace(' ', '') - params = {'Key-Type': 'RSA', - 'Key-Length': 2048, - 'Subkey-Type': 'RSA', - 'Subkey-Length': 2048, - 'Name-Comment': 'A test user', - 'Expire-Date': 0, - 'Name-Real': '%s %s' % (first_name, last_name), - 'Name-Email': ("%s.%s@%s" - % (first_name, last_name, domain)).lower()} - if passphrase is None: - passphrase = ("%s%s" % (first_name[0], last_name)).lower() - params['Passphrase'] = passphrase - cmd = self.gpg.gen_key_input(**params) - return self.gpg.gen_key(cmd) + ## XXX will GPG just use it's defaults? does it have defaults if + ## we've just given it a homedir without a gpg.conf? + key_type = 'RSA'if key_type is None else key_type + key_length = 2048 if key_length is None else key_length - def do_key_generation(self): - """Test that key generation succeeds""" - result = self.generate_key("Barbara", "Brown", "beta.com") - self.assertNotEqual(None, result, "Non-null result") - return result + batch = {'Key-Type': key_type, + 'Key-Length': key_length, + 'Name-Comment': 'python-gnupg tester', + 'Expire-Date': self.expire_today(), + 'Name-Real': '%s' % real_name, + 'Name-Email': ("%s.%s@%s" % (name, domain)) } + + batch['Passphrase'] = name if passphrase is None else passphrase + + if subkey_type is not None: + batch['Subkey-Type'] = subkey_type + batch['Subkey-Length'] = key_length + + key_input = self.gpg.gen_key_input(**batch) + return key_input + + def generate_key(self, real_name, email_domain, **kwargs): + """Generate a basic key""" + key_input = self.generate_key_input(real_name, email_domain, **kwargs): + key = self.gpg.gen_key(key_input) + + + def test_gen_key_input(self): + """Test that GnuPG batch file creation is successful.""" + key_input = self.generate_key_input("Francisco Ferrer", "an.ok") + + + def test_rsa_key_generation(self): + """Test that RSA key generation succeeds""" + key = self.generate_key("Barbara Brown", "beta.com") + self.assertIsNotNone(key.fingerprint) + + def test_rsa_key_generation_with_unicode(self): + """Test that RSA key generation succeeds with unicode characters.""" + key = self.generate_key("AnaĆÆs de Flavigny", "ĆŖtrerien.fr") + self.assertIsNotNone(key.fingerprint) + + def test_rsa_key_generation_with_subkey(self): + """Test that RSA key generation succeeds with additional subkey.""" + key = self.generate_key("Need Caffeine", "nowplea.se", + subkey_type='RSA') + self.assertIsNotNone(key.fingerprint) + + def test_dsa_key_generation(self): + """Test that DSA key generation succeeds""" + key = self.generate_key("DSA Signonly", "test.com") + self.assertIsNotNone(key.fingerprint) + + def test_dsa_key_generation_with_unicode(self): + """Test that DSA key generation succeeds with unicode characters.""" + key = self.generate_key("ē “å£ŠåˆčØˆć™ć‚‹", "ē “å£ŠåˆčØˆć™ć‚‹.ę—„ęœ¬") + self.assertIsNotNone(key.fingerprint) + + def test_dsa_key_generation_with_subkey(self): + """Test that RSA key generation succeeds with additional subkey.""" + key = self.generate_key("OMG Moar Coffee", "giveitto.me", + subkey_type='ELG-E') + self.assertIsNotNone(key.fingerprint) def test_key_generation_with_invalid_key_type(self): """Test that key generation handles invalid key type""" @@ -225,14 +276,15 @@ class GPGTestCase(unittest.TestCase): 'Subkey-Type': 'ELG-E', 'Subkey-Length': 2048, 'Name-Comment': 'A test user', - 'Expire-Date': 0, + 'Expire-Date': self.expire_tomorrow(), 'Name-Real': 'Test Name', 'Name-Email': 'test.name@example.com', } - cmd = self.gpg.gen_key_input(**params) - result = self.gpg.gen_key(cmd) - self.assertFalse(result.data, 'Null data result') - self.assertEqual(None, result.fingerprint, 'Null fingerprint result') + batch = self.gpg.gen_key_input(**params) + key = self.gpg.gen_key(batch) + self.assertIsInstance(key.data, str) + self.assertEquals(key.data, '') + self.assertIs(None, result.fingerprint, 'Null fingerprint result') def test_key_generation_with_colons(self): """Test that key generation handles colons in key fields""" @@ -489,6 +541,16 @@ TEST_GROUPS = { 'test_list_keys_initial_secret', 'test_make_args_drop_protected_options', 'test_make_args']), + 'genkey' : set(['test_gen_key_input', + 'test_rsa_key_generation', + 'test_rsa_key_generation_with_unicode', + 'test_rsa_key_generation_with_subkey', + 'test_dsa_key_generation', + 'test_dsa_key_generation_with_unicode', + 'test_dsa_key_generation_with_subkey', + 'test_key_generation_with_invalid_key_type', + 'test_key_generation_with_empty_value', + 'test_key_generation_with_colons']), 'sign' : set(['test_signature_verification']), 'crypt' : set(['test_encryption_and_decryption', From 09481d27ae5d7324f64ac14bda001fc1b8f4a2bb Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 9 Apr 2013 17:34:18 +0000 Subject: [PATCH 015/397] Split key material related unittests into separate test suites by functionality. --- test_gnupg.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/test_gnupg.py b/test_gnupg.py index debe0cc..d1dda4b 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -555,14 +555,11 @@ TEST_GROUPS = { 'crypt' : set(['test_encryption_and_decryption', 'test_file_encryption_and_decryption']), - 'key' : set(['test_deletion', - 'test_import_and_export', - 'test_public_keyring', - 'test_secret_keyring', - 'test_list_keys_after_generation', - 'test_key_generation_with_invalid_key_type', - 'test_key_generation_with_empty_value', - 'test_key_generation_with_colons']), + 'listkeys': set(['test_list_keys_after_generation']), + 'keyrings' set(['test_public_keyring', + 'test_secret_keyring', + 'test_import_and_export', + 'test_deletion']), 'import' : set(['test_import_only']), } From 9af49dbce5ada512f866fc484420dcedff518136 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 9 Apr 2013 17:35:10 +0000 Subject: [PATCH 016/397] Add missing option `--batch` to _allowed options set. * Rearranged the other options because their order was nolens volens. --- gnupg.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/gnupg.py b/gnupg.py index 01b65e3..9a44f42 100644 --- a/gnupg.py +++ b/gnupg.py @@ -406,12 +406,18 @@ def _is_allowed(input): ## eg, --no-show-photos would mitigate things like ## https://www-01.ibm.com/support/docview.wss?uid=swg21620982 _allowed = frozenset( - ['--list-packets', '--delete-keys', '--delete-secret-keys', - '--encrypt', '--print-mds', '--print-md', '--sign', - '--encrypt-files', '--gen-key', '--decrypt', '--decrypt-files', - '--list-keys', '--import', '--verify', '--version', - '--status-fd', '--no-tty', '--homedir', '--no-default-keyring', - '--keyring', '--passphrase-fd', '--fingerprint', '--with-colons']) + ['--list-keys', '--list-packets', '--with-colons', + '--delete-keys', '--delete-secret-keys', + '--encrypt', '--encrypt-files', + '--print-mds', '--print-md', '--sign', + '--gen-key', '--batch', + '--decrypt', '--decrypt-files', + '--import', + '--verify', + '--version', + '--status-fd', '--no-tty', '--passphrase-fd', + '--homedir', '--no-default-keyring', '--keyring', + '--fingerprint']) ## check that _allowed is a subset of _possible try: @@ -1760,7 +1766,7 @@ class GPG(object): :param always_trust: Instruct GnuPG to ignore trust checks. :param passphrase: The passphrase for the secret key used for decryption. :param output: A file to write the decrypted output to. - """ + """ args = ["--decrypt"] if output: # write the output to a file with the specified name if os.path.exists(output): From e9d223f701fcc959c6fbf850e17365a8b5d0fc2f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 9 Apr 2013 17:37:54 +0000 Subject: [PATCH 017/397] Fix typos, remove extra whitespace line. --- test_gnupg.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/test_gnupg.py b/test_gnupg.py index d1dda4b..b72f6c6 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -214,7 +214,7 @@ class GPGTestCase(unittest.TestCase): 'Name-Comment': 'python-gnupg tester', 'Expire-Date': self.expire_today(), 'Name-Real': '%s' % real_name, - 'Name-Email': ("%s.%s@%s" % (name, domain)) } + 'Name-Email': ("%s@%s" % (name, email_domain)) } batch['Passphrase'] = name if passphrase is None else passphrase @@ -227,10 +227,9 @@ class GPGTestCase(unittest.TestCase): def generate_key(self, real_name, email_domain, **kwargs): """Generate a basic key""" - key_input = self.generate_key_input(real_name, email_domain, **kwargs): + key_input = self.generate_key_input(real_name, email_domain, **kwargs) key = self.gpg.gen_key(key_input) - def test_gen_key_input(self): """Test that GnuPG batch file creation is successful.""" key_input = self.generate_key_input("Francisco Ferrer", "an.ok") @@ -276,7 +275,7 @@ class GPGTestCase(unittest.TestCase): 'Subkey-Type': 'ELG-E', 'Subkey-Length': 2048, 'Name-Comment': 'A test user', - 'Expire-Date': self.expire_tomorrow(), + 'Expire-Date': self.expire_today(), 'Name-Real': 'Test Name', 'Name-Email': 'test.name@example.com', } @@ -556,10 +555,10 @@ TEST_GROUPS = { 'crypt' : set(['test_encryption_and_decryption', 'test_file_encryption_and_decryption']), 'listkeys': set(['test_list_keys_after_generation']), - 'keyrings' set(['test_public_keyring', - 'test_secret_keyring', - 'test_import_and_export', - 'test_deletion']), + 'keyrings': set(['test_public_keyring', + 'test_secret_keyring', + 'test_import_and_export', + 'test_deletion']), 'import' : set(['test_import_only']), } From 1cac3df2cab66f00bb6abd98e6141e53a63c518b Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 16:43:42 +0000 Subject: [PATCH 018/397] Add the missing crunchbang in test_gnupg.py. --- test_gnupg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test_gnupg.py b/test_gnupg.py index b72f6c6..65d8bda 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # -*- coding: utf-8 -*- """ A test harness for gnupg.py. From 4ee62ea400806979351871161a76b0296f17fc56 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 16:52:48 +0000 Subject: [PATCH 019/397] Patch Python2.6 and earlier to use future-patched unittest module. --- test_gnupg.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test_gnupg.py b/test_gnupg.py index 65d8bda..fecbaf7 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -14,7 +14,12 @@ import os import shutil import sys import tempfile -import unittest + +## Use unittest2 if we're on Python2.6 or less: +if sys.version_info.major == 2 and sys.version_info.minor <= 6: + unittest = __import__(unittest2) +else: + import unittest import gnupg From c5d2e6fc9d112d7bae041632f100ce0b44018536 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 16:54:39 +0000 Subject: [PATCH 020/397] Keep module metadata in one place. --- test_gnupg.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test_gnupg.py b/test_gnupg.py index fecbaf7..edfa183 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -23,8 +23,9 @@ else: import gnupg -__author__ = "Isis Lovecruft" -__date__ = "2013-03-02" +__author__ = gnupg.__author__ +__date__ = gnupg.__date__ +__version__ = gnupg.__version__ ALL_TESTS = True REPO_DIR = os.getcwd() From c72aa1eb96eb0fa920ffadeb4e4cf4b7bce351a0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 16:55:29 +0000 Subject: [PATCH 021/397] Update unittest temporary file directory location and gnupghome. --- test_gnupg.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test_gnupg.py b/test_gnupg.py index edfa183..5fbb453 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -27,11 +27,10 @@ __author__ = gnupg.__author__ __date__ = gnupg.__date__ __version__ = gnupg.__version__ -ALL_TESTS = True REPO_DIR = os.getcwd() -TEST_DIR = os.path.join(REPO_DIR, 'keys') +HOME_DIR = os.path.join(REPO_DIR, 'keys') -tempfile.tempdir = os.path.join(REPO_DIR, 'temp') +tempfile.tempdir = os.path.join(REPO_DIR, 'tmp_test') if not os.path.isdir(tempfile.gettempdir()): os.mkdir(tempfile.gettempdir()) From 5cd2eb2b94114e59aa07135689f73296897972a2 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 16:58:59 +0000 Subject: [PATCH 022/397] Also get the logger from gnupg so that we can debug events in unittests. --- test_gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_gnupg.py b/test_gnupg.py index 5fbb453..015cb0f 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -39,7 +39,7 @@ def _make_tempfile(*args, **kwargs): return tempfile.TemporaryFile(dir=tempfile.gettempdir(), *args, **kwargs) -logger = logging.getLogger(__name__) +logger = logging.getLogger(gnupg.logger.name) KEYS_TO_IMPORT = """-----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1.4.9 (MingW32) From 5e706d09d08e8a9ce6f43da2bd3b8a3e55bbcfa2 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 17:00:11 +0000 Subject: [PATCH 023/397] Pep8 the docstrings. --- test_gnupg.py | 57 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/test_gnupg.py b/test_gnupg.py index 015cb0f..f24e7e0 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -101,13 +101,16 @@ def is_list_with_len(o, n): return isinstance(o, list) and len(o) == n def compare_keys(k1, k2): - """Compare ASCII keys""" + """ + Compare ASCII keys. + """ k1 = k1.split('\n') k2 = k2.split('\n') del k1[1] # remove version lines del k2[1] return k1 != k2 + class ResultStringIO(io.StringIO): def __init__(self): super(self, io.StringIO).__init__() @@ -115,9 +118,27 @@ class ResultStringIO(io.StringIO): def write(self, data): super(self, io.StringIO).write(unicode(data)) + class GPGTestCase(unittest.TestCase): + """ + A group of :class:`unittest.TestCase` unittests for testing python-gnupg. + """ + + @classmethod + def setUpClass(cls): + """ + Setup the :class:`GPGTestCase` and runtime environment for tests. + + This function must be called manually. + xxx or is called by TestSuite. + """ + pass + def setUp(self): - hd = os.path.join(os.getcwd(), 'keys') + """ + This method is called once per self.test_* method. + """ + hd = HOME_DIR if os.path.exists(hd): self.assertTrue(os.path.isdir(hd), "Not a directory: %s" % hd) @@ -128,20 +149,26 @@ class GPGTestCase(unittest.TestCase): self.secring = os.path.join(self.homedir, 'secring.gpg') def test_environment(self): - """Test the environment by ensuring that setup worked""" + """ + Test the environment by ensuring that setup worked. + """ hd = self.homedir self.assertTrue(os.path.exists(hd) and os.path.isdir(hd), "Not an existing directory: %s" % hd) def test_gpg_binary(self): - """Test that 'gpg --version' does not return an error code""" + """ + Test that 'gpg --version' does not return an error code. + """ proc = self.gpg._open_subprocess(['--version']) result = io.StringIO() self.gpg._collect_output(proc, result, stdin=proc.stdin) self.assertEqual(proc.returncode, 0) def test_gpg_binary_version_str(self): - """That that 'gpg --version' returns the expected output""" + """ + That that 'gpg --version' returns the expected output. + """ proc = self.gpg._open_subprocess(['--version']) result = proc.stdout.read(1024) expected1 = "Supported algorithms:" @@ -155,11 +182,15 @@ class GPGTestCase(unittest.TestCase): self.assertGreater(result.find(expected4), 0) def test_gpg_binary_not_abs(self): - """Test that a non-absolute path to gpg results in a full path""" + """ + Test that a non-absolute path to gpg results in a full path. + """ self.assertTrue(os.path.isabs(self.gpg.gpgbinary)) def test_make_args_drop_protected_options(self): - """Test that unsupported gpg options are dropped""" + """ + Test that unsupported gpg options are dropped. + """ self.gpg.options = ['--tyrannosaurus-rex', '--stegosaurus'] self.gpg.keyring = self.secring cmd = self.gpg.make_args(None, False) @@ -170,7 +201,9 @@ class GPGTestCase(unittest.TestCase): self.assertListEqual(cmd, expected) def test_make_args(self): - """Test argument line construction""" + """ + Test argument line construction. + """ not_allowed = ['--bicycle', '--zeppelin', 'train', 'flying-carpet'] self.gpg.options = not_allowed[:-2] args = self.gpg.make_args(not_allowed[2:], False) @@ -179,14 +212,18 @@ class GPGTestCase(unittest.TestCase): self.assertNotIn(na, args) def test_list_keys_initial_public(self): - """Test that initially there are no public keys""" + """ + Test that initially there are no public keys. + """ public_keys = self.gpg.list_keys() self.assertTrue(is_list_with_len(public_keys, 0), "Empty list expected...got instead: %s" % str(public_keys)) def test_list_keys_initial_secret(self): - """Test that initially there are no secret keys""" + """ + Test that initially there are no secret keys. + """ private_keys = self.gpg.list_keys(secret=True) self.assertTrue(is_list_with_len(private_keys, 0), "Empty list expected...got instead: %s" From fc71ede5026f64dfa1542f9f407da0bfc3de7b22 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 17:00:52 +0000 Subject: [PATCH 024/397] Cleanup assertion statement. --- test_gnupg.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test_gnupg.py b/test_gnupg.py index f24e7e0..38148e1 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -140,8 +140,7 @@ class GPGTestCase(unittest.TestCase): """ hd = HOME_DIR if os.path.exists(hd): - self.assertTrue(os.path.isdir(hd), - "Not a directory: %s" % hd) + self.assertTrue(os.path.isdir(hd), "Not a directory: %s" % hd) shutil.rmtree(hd) self.homedir = hd self.gpg = gnupg.GPG(gpghome=hd, gpgbinary='gpg') From 5f157c3c00f223a9a691694063603c502b39e9ff Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 17:01:37 +0000 Subject: [PATCH 025/397] Don't hardcode paths. --- test_gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_gnupg.py b/test_gnupg.py index 38148e1..a3c16a9 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -195,7 +195,7 @@ class GPGTestCase(unittest.TestCase): cmd = self.gpg.make_args(None, False) expected = ['/usr/bin/gpg', '--status-fd 2 --no-tty', - '--homedir "/home/isis/code/riseup/python-gnupg/keys"', + '--homedir "%s"' % os.path.join(os.getcwd(), 'keys'), '--no-default-keyring --keyring "%s"' % self.secring] self.assertListEqual(cmd, expected) From b758239af852140a5e77ea09b0c172f207bef597 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 17:02:13 +0000 Subject: [PATCH 026/397] Add implementation note on test_copy_data(). --- test_gnupg.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test_gnupg.py b/test_gnupg.py index a3c16a9..1cf8e32 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -230,7 +230,12 @@ class GPGTestCase(unittest.TestCase): def test_copy_data(self): - """Test that _copy_data() is able to duplicate byte streams""" + """ + XXX implement me + XXX add me to a test suite + + Test that _copy_data() is able to duplicate byte streams. + """ instream = io.BytesIO("This is a string of bytes mapped in memory.") outstream = str("And this one is just a string.") From 1d34b04d82d42fbf8d8aeb355eeacd7b85e73730 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 17:02:37 +0000 Subject: [PATCH 027/397] Remove unused function expire_today(). --- test_gnupg.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/test_gnupg.py b/test_gnupg.py index 1cf8e32..d3586f3 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -239,13 +239,6 @@ class GPGTestCase(unittest.TestCase): instream = io.BytesIO("This is a string of bytes mapped in memory.") outstream = str("And this one is just a string.") - def expire_today(self): - """Make an expiry date set for today""" - from datetime import datetime - today = datetime.today() - date = "%s" % today.date() - return date - def generate_key_input(self, real_name, email_domain, key_length=None, key_type=None, subkey_type=None, passphrase=None): """Generate a GnuPG batch file for key unattended key creation""" @@ -259,7 +252,7 @@ class GPGTestCase(unittest.TestCase): batch = {'Key-Type': key_type, 'Key-Length': key_length, 'Name-Comment': 'python-gnupg tester', - 'Expire-Date': self.expire_today(), + 'Expire-Date': 1, 'Name-Real': '%s' % real_name, 'Name-Email': ("%s@%s" % (name, email_domain)) } From f0ee442bab62653beb5abee24f32c26bd3b6b482 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 17:03:57 +0000 Subject: [PATCH 028/397] Pep8ify some more docstrings. --- test_gnupg.py | 84 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 21 deletions(-) diff --git a/test_gnupg.py b/test_gnupg.py index d3586f3..eef4a1f 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -241,7 +241,9 @@ class GPGTestCase(unittest.TestCase): def generate_key_input(self, real_name, email_domain, key_length=None, key_type=None, subkey_type=None, passphrase=None): - """Generate a GnuPG batch file for key unattended key creation""" + """ + Generate a GnuPG batch file for key unattended key creation. + """ name = real_name.lower().replace(' ', '') ## XXX will GPG just use it's defaults? does it have defaults if @@ -266,49 +268,67 @@ class GPGTestCase(unittest.TestCase): return key_input def generate_key(self, real_name, email_domain, **kwargs): - """Generate a basic key""" + """ + Generate a basic key. + """ key_input = self.generate_key_input(real_name, email_domain, **kwargs) key = self.gpg.gen_key(key_input) def test_gen_key_input(self): - """Test that GnuPG batch file creation is successful.""" + """ + Test that GnuPG batch file creation is successful. + """ key_input = self.generate_key_input("Francisco Ferrer", "an.ok") def test_rsa_key_generation(self): - """Test that RSA key generation succeeds""" + """ + Test that RSA key generation succeeds. + """ key = self.generate_key("Barbara Brown", "beta.com") self.assertIsNotNone(key.fingerprint) def test_rsa_key_generation_with_unicode(self): - """Test that RSA key generation succeeds with unicode characters.""" + """ + Test that RSA key generation succeeds with unicode characters. + """ key = self.generate_key("AnaĆÆs de Flavigny", "ĆŖtrerien.fr") self.assertIsNotNone(key.fingerprint) def test_rsa_key_generation_with_subkey(self): - """Test that RSA key generation succeeds with additional subkey.""" + """ + Test that RSA key generation succeeds with additional subkey. + """ key = self.generate_key("Need Caffeine", "nowplea.se", subkey_type='RSA') self.assertIsNotNone(key.fingerprint) def test_dsa_key_generation(self): - """Test that DSA key generation succeeds""" + """ + Test that DSA key generation succeeds. + """ key = self.generate_key("DSA Signonly", "test.com") self.assertIsNotNone(key.fingerprint) def test_dsa_key_generation_with_unicode(self): - """Test that DSA key generation succeeds with unicode characters.""" + """ + Test that DSA key generation succeeds with unicode characters. + """ key = self.generate_key("ē “å£ŠåˆčØˆć™ć‚‹", "ē “å£ŠåˆčØˆć™ć‚‹.ę—„ęœ¬") self.assertIsNotNone(key.fingerprint) def test_dsa_key_generation_with_subkey(self): - """Test that RSA key generation succeeds with additional subkey.""" + """ + Test that RSA key generation succeeds with additional subkey. + """ key = self.generate_key("OMG Moar Coffee", "giveitto.me", subkey_type='ELG-E') self.assertIsNotNone(key.fingerprint) def test_key_generation_with_invalid_key_type(self): - """Test that key generation handles invalid key type""" + """ + Test that key generation handles invalid key type. + """ params = { 'Key-Type': 'INVALID', 'Key-Length': 1024, @@ -326,7 +346,9 @@ class GPGTestCase(unittest.TestCase): self.assertIs(None, result.fingerprint, 'Null fingerprint result') def test_key_generation_with_colons(self): - """Test that key generation handles colons in key fields""" + """ + Test that key generation handles colons in Name fields. + """ params = { 'key_type': 'RSA', 'name_real': 'urn:uuid:731c22c4-830f-422f-80dc-14a9fdae8c19', @@ -345,7 +367,9 @@ class GPGTestCase(unittest.TestCase): '(dummy comment) ') def test_key_generation_with_empty_value(self): - """Test that key generation handles empty values""" + """ + Test that key generation handles empty values. + """ params = { 'key_type': 'RSA', 'key_length': 1024, @@ -358,7 +382,9 @@ class GPGTestCase(unittest.TestCase): self.assertTrue('\nName-Comment: A\n' in cmd) def test_list_keys_after_generation(self): - """Test that after key generation, the generated key is available""" + """ + Test that after key generation, the generated key is available. + """ self.test_list_keys_initial() self.do_key_generation() public_keys = self.gpg.list_keys() @@ -369,7 +395,9 @@ class GPGTestCase(unittest.TestCase): "1-element list expected") def test_encryption_and_decryption(self): - """Test that encryption and decryption works""" + """ + Test that encryption and decryption works. + """ logger.debug("test_encryption_and_decryption begins") key = self.generate_key("Andrew", "Able", "alpha.com", passphrase="andy") @@ -404,17 +432,23 @@ class GPGTestCase(unittest.TestCase): self.assertEqual(data, str(ddata)) def test_public_keyring(self): - """Test that the public keyring is found in the gpg home directory""" + """ + Test that the public keyring is found in the gpg home directory. + """ self.gpg.keyring = self.pubring self.assertTrue(os.path.isfile(self.pubring)) def test_secret_keyring(self): - """Test that the secret keyring is found in the gpg home directory""" + """ + Test that the secret keyring is found in the gpg home directory. + """ self.gpg.keyring = self.secring self.assertTrue(os.path.isfile(self.secring)) def test_import_and_export(self): - """Test that key import and export works""" + """ + Test that key import and export works. + """ logger.debug("test_import_and_export begins") self.test_list_keys_initial() gpg = self.gpg @@ -443,7 +477,9 @@ class GPGTestCase(unittest.TestCase): logger.debug("test_import_and_export ends") def test_import_only(self): - """Test that key import works""" + """ + Test that key import works. + """ logger.debug("test_import_only begins") self.test_list_keys_initial() self.gpg.import_keys(KEYS_TO_IMPORT) @@ -465,7 +501,9 @@ class GPGTestCase(unittest.TestCase): logger.debug("test_import_only ends") def test_signature_verification(self): - """Test that signing and verification works""" + """ + Test that signing and verification works. + """ logger.debug("test_signature_verification begins") key = self.generate_key("Andrew", "Able", "alpha.com") self.gpg.encoding = 'latin-1' @@ -523,7 +561,9 @@ class GPGTestCase(unittest.TestCase): logger.debug("test_signature_verification ends") def test_deletion(self): - """Test that key deletion works""" + """ + Test that key deletion works. + """ logger.debug("test_deletion begins") self.gpg.import_keys(KEYS_TO_IMPORT) public_keys = self.gpg.list_keys() @@ -536,7 +576,9 @@ class GPGTestCase(unittest.TestCase): logger.debug("test_deletion ends") def test_file_encryption_and_decryption(self): - """Test that encryption/decryption to/from file works""" + """ + Test that encryption/decryption to/from file works. + """ logger.debug("test_file_encryption_and_decryption begins") encfname = _make_tempfile() From c19fd30545d69c030a248a4478c730bd0cd82803 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 17:05:35 +0000 Subject: [PATCH 029/397] Add assertions to check that key have been created in test_gen_key_*(). --- test_gnupg.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test_gnupg.py b/test_gnupg.py index eef4a1f..973d615 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -286,6 +286,7 @@ class GPGTestCase(unittest.TestCase): Test that RSA key generation succeeds. """ key = self.generate_key("Barbara Brown", "beta.com") + self.assertIsNotNone(key) self.assertIsNotNone(key.fingerprint) def test_rsa_key_generation_with_unicode(self): @@ -293,6 +294,7 @@ class GPGTestCase(unittest.TestCase): Test that RSA key generation succeeds with unicode characters. """ key = self.generate_key("AnaĆÆs de Flavigny", "ĆŖtrerien.fr") + self.assertIsNotNone(key) self.assertIsNotNone(key.fingerprint) def test_rsa_key_generation_with_subkey(self): @@ -301,6 +303,7 @@ class GPGTestCase(unittest.TestCase): """ key = self.generate_key("Need Caffeine", "nowplea.se", subkey_type='RSA') + self.assertIsNotNone(key) self.assertIsNotNone(key.fingerprint) def test_dsa_key_generation(self): @@ -308,6 +311,7 @@ class GPGTestCase(unittest.TestCase): Test that DSA key generation succeeds. """ key = self.generate_key("DSA Signonly", "test.com") + self.assertIsNotNone(key) self.assertIsNotNone(key.fingerprint) def test_dsa_key_generation_with_unicode(self): @@ -315,6 +319,7 @@ class GPGTestCase(unittest.TestCase): Test that DSA key generation succeeds with unicode characters. """ key = self.generate_key("ē “å£ŠåˆčØˆć™ć‚‹", "ē “å£ŠåˆčØˆć™ć‚‹.ę—„ęœ¬") + self.assertIsNotNone(key) self.assertIsNotNone(key.fingerprint) def test_dsa_key_generation_with_subkey(self): @@ -323,6 +328,7 @@ class GPGTestCase(unittest.TestCase): """ key = self.generate_key("OMG Moar Coffee", "giveitto.me", subkey_type='ELG-E') + self.assertIsNotNone(key) self.assertIsNotNone(key.fingerprint) def test_key_generation_with_invalid_key_type(self): @@ -343,7 +349,7 @@ class GPGTestCase(unittest.TestCase): key = self.gpg.gen_key(batch) self.assertIsInstance(key.data, str) self.assertEquals(key.data, '') - self.assertIs(None, result.fingerprint, 'Null fingerprint result') + self.assertIs(None, key.fingerprint, 'Null fingerprint result') def test_key_generation_with_colons(self): """ From 43a69e9fcd73612a67a54ef2c8ea66ade0fe415f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 17:08:21 +0000 Subject: [PATCH 030/397] Update test assertions for key generation and batch parameters after import. * In unittest test_key_generation_import_list_with_colons(). --- test_gnupg.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/test_gnupg.py b/test_gnupg.py index 973d615..4213646 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -361,9 +361,25 @@ class GPGTestCase(unittest.TestCase): 'name_comment': 'dummy comment', 'name_email': 'test.name@example.com', } - cmd = self.gpg.gen_key_input(**params) - result = self.gpg.gen_key(cmd) + batch = self.gpg.gen_key_input(**params) + key = self.gpg.gen_key(cmd) + print "KEY DATA\n", key.data + print "KEY FINGERPRINT\n", key.fingerprint + + def test_key_generation_import_list_with_colons(self): + """ + Test that key generation handles colons in Name fields. + """ + params = { + 'key_type': 'RSA', + 'name_real': 'urn:uuid:731c22c4-830f-422f-80dc-14a9fdae8c19', + 'name_comment': 'dummy comment', + 'name_email': 'test.name@example.com', + } + batch = self.gpg.gen_key_input(**params) + key = self.gpg.gen_key(cmd) keys = self.gpg.list_keys() + self.assertIsNotNone(key) self.assertEqual(len(keys), 1) key = keys[0] uids = key['uids'] From 20284e07419889c0e788cdd3d1dadbbe350170c1 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 17:14:06 +0000 Subject: [PATCH 031/397] Revise logger and suite ordering, and override parts of unittest module. * Rearrange sets of testsuites to provide logical execution order. * Remove unused suite() function. * Create dynamic module loader through a unittest.TestProgram.createTest override to take an instance of unittest.TestLoader which is created later. --- test_gnupg.py | 129 +++++++++++++++++++++++++++----------------------- 1 file changed, 69 insertions(+), 60 deletions(-) diff --git a/test_gnupg.py b/test_gnupg.py index 4213646..e8ac356 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -635,75 +635,84 @@ class GPGTestCase(unittest.TestCase): logger.debug("test_file_encryption_and_decryption ends") -TEST_GROUPS = { - 'basic' : set(['test_environment', - 'test_gpg_binary', - 'test_gpg_binary_not_abs', - 'test_gpg_binary_version_str', - 'test_list_keys_initial_public', - 'test_list_keys_initial_secret', - 'test_make_args_drop_protected_options', - 'test_make_args']), - 'genkey' : set(['test_gen_key_input', - 'test_rsa_key_generation', - 'test_rsa_key_generation_with_unicode', - 'test_rsa_key_generation_with_subkey', - 'test_dsa_key_generation', - 'test_dsa_key_generation_with_unicode', - 'test_dsa_key_generation_with_subkey', - 'test_key_generation_with_invalid_key_type', - 'test_key_generation_with_empty_value', - 'test_key_generation_with_colons']), - 'sign' : set(['test_signature_verification']), +suites = { 'basic': set(['test_environment', + 'test_gpg_binary', + 'test_gpg_binary_not_abs', + 'test_gpg_binary_version_str', + 'test_list_keys_initial_public', + 'test_list_keys_initial_secret', + 'test_make_args_drop_protected_options', + 'test_make_args']), + 'genkey': set(['test_gen_key_input', + 'test_rsa_key_generation', + 'test_rsa_key_generation_with_unicode', + 'test_rsa_key_generation_with_subkey', + 'test_dsa_key_generation', + 'test_dsa_key_generation_with_unicode', + 'test_dsa_key_generation_with_subkey', + 'test_key_generation_with_invalid_key_type', + 'test_key_generation_with_empty_value', + 'test_key_generation_with_colons']), + 'sign': set(['test_signature_verification']), + 'crypt': set(['test_encryption_and_decryption', + 'test_file_encryption_and_decryption']), + 'listkeys': set(['test_list_keys_after_generation']), + 'keyrings': set(['test_public_keyring', + 'test_secret_keyring', + 'test_import_and_export', + 'test_deletion']), + 'import': set(['test_import_only']), } - 'crypt' : set(['test_encryption_and_decryption', - 'test_file_encryption_and_decryption']), - 'listkeys': set(['test_list_keys_after_generation']), - 'keyrings': set(['test_public_keyring', - 'test_secret_keyring', - 'test_import_and_export', - 'test_deletion']), - 'import' : set(['test_import_only']), - } - -def suite(args=None): - if args is None: - args = sys.argv[1:] - if not args: - result = unittest.TestLoader().loadTestsFromTestCase(GPGTestCase) - want_doctests = False - else: - tests = set() - want_doctests = False - for arg in args: - if arg in TEST_GROUPS: - tests.update(TEST_GROUPS[arg]) - elif arg == "doc": - want_doctests = True - else: - print("Ignoring unknown test group %r" % arg) - result = unittest.TestSuite(list(map(GPGTestCase, tests))) - if want_doctests: - result.addTest(doctest.DocTestSuite(gnupg)) - return result - -def init_logging(): +def _init_logging(): logging.basicConfig( level=logging.DEBUG, filename="test_gnupg.log", filemode="a", format="%(asctime)s %(levelname)-5s %(name)-7s %(threadName)-10s %(message)s") logging.captureWarnings(True) logging.logThreads = True - logger.addHandler(logging.StreamHandler(stream=sys.stdout)) - #logger.addHandler(logging.RootLogger(logging.DEBUG)) - #logger.addHandler(logging.Logger("gnupg.py", level=logging.DEBUG)) + stream_handler = logging.StreamHandler(stream=sys.stdout) + stream_handler.setLevel(logging.DEBUG) + logger = gnupg.logger + logger.addHandler(stream_handler) + logger.debug("Starting the logger...") -def main(): - init_logging() - tests = suite() - results = unittest.TextTestRunner(verbosity=3).run(tests) - return not results.wasSuccessful() +def main(args): + if not args.quiet: + _init_logging() + loader = unittest.TestLoader() + + def _createTests(prog): + load_tests = list() + if args.test is not None: + for suite in args.test: + if suite in args.suites.keys(): + logger.debug("Adding %d items from test suite '%s':" + % (len(args.suites[suite]), suite)) + for method in args.suites[suite]: + load_tests.append(method) + logger.debug("\t%s" % method) + else: + logger.debug("Ignoring unknown test suite %r" % suite) + tests = unittest.TestSuite(list(map(GPGTestCase, load_tests))) + else: + tests = prog.testLoader.loadTestsFromTestCase(GPGTestCase) + args.run_doctest = True ## xxx can we set options here? + if args.run_doctest: + tests.addTest(doctest.DocTestSuite(gnupg)) + logger.debug("Loaded %d tests..." % tests.countTestCases()) + prog.test = tests + + runner = unittest.TextTestRunner(verbosity=args.verbose, stream=sys.stderr) + runner.resultclass = unittest.TextTestResult + + prog = unittest.TestProgram + prog.createTests = _createTests + program = prog(module=GPGTestCase, + testRunner=runner, + testLoader=loader, + verbosity=args.verbose, + catchbreak=True) if __name__ == "__main__": sys.exit(main()) From a97040a06f6be6ce3700f2a5e020fb49ba22f118 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 17:19:25 +0000 Subject: [PATCH 032/397] Add unittest cmdline argparse to hook into unittest.TestProgram's parser. --- test_gnupg.py | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/test_gnupg.py b/test_gnupg.py index e8ac356..88a7dcd 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -6,9 +6,12 @@ A test harness for gnupg.py. Copyright Ā© 2013 Isis Lovecruft. Copyright Ā© 2008-2013 Vinay Sajip. All rights reserved. """ + +import argparse import doctest import logging from functools import wraps +import inspect import io import os import shutil @@ -715,4 +718,41 @@ def main(args): catchbreak=True) if __name__ == "__main__": - sys.exit(main()) + + suite_names = list() + for name, methodset in suites.items(): + suite_names.append(name) + this_file = inspect.getfile(inspect.currentframe()).split('.', 1)[0] + #mod = getattr(this_file, '__dict__', None) + #func = getattr(gnupg.__module__, '__setattr__', None) + #if func is not None: + # func(name, list(methodset)) + setattr(GPGTestCase, name, list(methodset)) + + parser = argparse.ArgumentParser(description="Unittests for python-gnupg") + parser.add_argument('--doctest', + dest='run_doctest', + type=bool, + default=False, + help='Run example code in docstrings') + parser.add_argument('--quiet', + dest='quiet', + type=bool, + default=False, + help='Disable logging to stdout') + parser.add_argument('--verbose', + dest='verbose', + type=int, + default=4, + help='Set verbosity level (low=1 high=5) (default: 4)') + parser.add_argument('test', + metavar='test', + nargs='+', + type=str, + help='Select a test suite to run (default: all)') + parser.epilog = "Available test suites: %s" % " ".join(suite_names) + + args = parser.parse_args() + args.suites = suites + + sys.exit(main(args)) From e978f3471235800cb3758d8a2a221a560300d3f2 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 17:20:47 +0000 Subject: [PATCH 033/397] Add '--secret-keyring' as an allowed option. --- gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg.py b/gnupg.py index 9a44f42..e5ba082 100644 --- a/gnupg.py +++ b/gnupg.py @@ -416,7 +416,7 @@ def _is_allowed(input): '--verify', '--version', '--status-fd', '--no-tty', '--passphrase-fd', - '--homedir', '--no-default-keyring', '--keyring', + '--homedir', '--no-default-keyring', '--keyring', '--secret-keyring', '--fingerprint']) ## check that _allowed is a subset of _possible From 5fd040c793ae6bd0dec727005e3f3f40f54dbeb4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 17:21:11 +0000 Subject: [PATCH 034/397] Remove extraneous logger statement. --- gnupg.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/gnupg.py b/gnupg.py index e5ba082..0dcc7af 100644 --- a/gnupg.py +++ b/gnupg.py @@ -448,8 +448,6 @@ def _is_allowed(input): raise ProtectedOption("Option '%s' not supported." % _fix_unsafe(hyphenated)) else: - logger.debug("Got allowed option '%s'." - % _fix_unsafe(hyphenated)) return input return None From af7e952b420599f4753f11133b48e84eb7eac8d0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 17:22:09 +0000 Subject: [PATCH 035/397] Factor out some unnecessary string parsing code. --- gnupg.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/gnupg.py b/gnupg.py index 0dcc7af..541e806 100644 --- a/gnupg.py +++ b/gnupg.py @@ -541,19 +541,14 @@ def _sanitise(*args): else: safe_values += (allowed_flag + " ") if isinstance(value, str): - value_list = [] - if value.find(' ') > 0: - value_list = value.split(' ') - else: - logger.debug("_check_values(): got non-string for values") + value_list = value.split(' ') for value in value_list: safe_value = _fix_unsafe(value) if allowed_flag == '--encrypt' or '--encrypt-files' \ or '--decrypt' or '--decrypt-file' \ or '--import' or '--verify': - ## xxx what other things should we check for? ## Place checks here: - if _is_file(safe_value): + if not safe_value == "" and _is_file(safe_value): safe_values += (safe_value + " ") else: logger.debug("Got non-filename for %s option: %s" From 0c5fda7da4b8b5c939e77086de125efc1aa3220d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 17:22:59 +0000 Subject: [PATCH 036/397] GnuPG wants pubring for its option '--keyring'. Fuck GnuPG. --- gnupg.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gnupg.py b/gnupg.py index 541e806..aeb6814 100644 --- a/gnupg.py +++ b/gnupg.py @@ -1076,7 +1076,7 @@ class GPG(object): to. :type keyring: C{str} - :param keyring: raises C{DeprecationWarning}. Use :param:secring. + :param keyring: raises C{DeprecationWarning}. Use :param:pubring. :type secring: C{str} :param secring: Name of alternative secret keyring file to use. If left @@ -1140,7 +1140,7 @@ class GPG(object): except DeprecationWarning as dw: log.warn(dw.message) finally: - secring = keyring + pubring = keyring secring = 'secring.gpg' if secring is None else _fix_unsafe(secring) pubring = 'pubring.gpg' if pubring is None else _fix_unsafe(pubring) @@ -1149,7 +1149,7 @@ class GPG(object): self.pubring = os.path.join(self.gpghome, pubring) ## XXX should eventually be changed throughout to 'secring', but until ## then let's not break backward compatibility - self.keyring = self.secring + self.keyring = self.pubring for ring in [self.secring, self.pubring]: if ring and not os.path.isfile(ring): @@ -1203,7 +1203,8 @@ class GPG(object): if self.gpghome: cmd.append('--homedir "%s"' % self.gpghome) if self.keyring: - cmd.append('--no-default-keyring --keyring "%s"' % self.keyring) + cmd.append('--no-default-keyring --keyring %s --secret-keyring %s' + % (self.pubring, self.secring)) if passphrase: cmd.append('--batch --passphrase-fd 0') if self.use_agent: From 9e0e0a0d4e8b81ec43ccbd9e8d1daf2cd86713c7 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 8 Apr 2013 16:44:18 +0000 Subject: [PATCH 037/397] Add Makefile command for creating a virtualenv with virtualenvwrapper. --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index ab43041..5792a18 100644 --- a/Makefile +++ b/Makefile @@ -26,3 +26,6 @@ docs: cd docs make clean make html + +venv: + -source /usr/shared/python/ns/virtualenvwrapper.sh && mkvirtualenv -a "$PWD" --unzip-setuptools --distribute python-gnupg From f385b3938ffae751403b9277e4e83aca8dd792c8 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 9 Apr 2013 08:37:14 +0000 Subject: [PATCH 038/397] Fix distutils, PyPI, and pip packaging configuration files. * Remove/replace old classifiers in PyPI configuration PKG-INFO. * Add pip requirements.txt file for documentation builds on readthedocs.org. * Fix package build variables in distutils setup.py. --- PKG-INFO | 43 ++++++++++++++++++++++--------------------- requirements.txt | 1 + setup.py | 27 ++++++++++++++------------- 3 files changed, 37 insertions(+), 34 deletions(-) create mode 100644 requirements.txt diff --git a/PKG-INFO b/PKG-INFO index 7acb3ca..206db4c 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -5,24 +5,25 @@ Summary: A wrapper for the Gnu Privacy Guard (GPG or GnuPG) Home-page: https://www.github.com/isislovecruft/python-gnupg Author: Isis Lovecruft Author-email: isis@leap.se -License: Copyright (C) 2013 by Isis Lovecruft. All Rights Reserved. See LICENSE for license. See COPYLEFT for "copyright". -Download-URL: https://github.com/isislovecruft/python-gnupg.git -Description: This module allows easy access to GnuPG's key management, encryption and signature functionality from Python programs. It is intended for use with Python 2.4 or greater. -Platform: No particular restrictions -Classifier: Development Status :: 3 - Alpha -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+) -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 2.4 -Classifier: Programming Language :: Python :: 2.5 -Classifier: Programming Language :: Python :: 2.6 -Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3.0 -Classifier: Programming Language :: Python :: 3.1 -Classifier: Programming Language :: Python :: 3.2 -Classifier: Operating System :: OS Independent -Classifier: Topic :: Security :: Cryptography -Classifier: Topic :: Software Development :: Libraries :: Python Modules -Classifier: Topic :: Utilities +Download-URL: https://github.com/isislovecruft/python-gnupg/archives/develop.zip +Description:: This module allows easy access to GnuPG's key management, encryption and signature functionality from Python programs. It is intended for use with Python 2.6 or greater. +Classifier:: Development Status :: 3 - Alpha +License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+) +Operating System :: MacOS :: MacOS X +Operating System :: Microsoft :: Windows :: Windows 7 +Operating System :: Microsoft :: Windows :: Windows XP +Operating System :: POSIX :: BSD +Operating System :: POSIX :: Linux +Classifier:: Intended Audience :: Developers +Classifier:: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+) +Classifier:: Programming Language :: Python +Classifier:: Programming Language :: Python :: 2 +Classifier:: Programming Language :: Python :: 3 +Classifier:: Programming Language :: Python :: 2.6 +Classifier:: Programming Language :: Python :: 2.7 +Classifier:: Programming Language :: Python :: 3.0 +Classifier:: Programming Language :: Python :: 3.1 +Classifier:: Programming Language :: Python :: 3.2 +Classifier:: Topic :: Security :: Cryptography +Classifier:: Topic :: Software Development :: Libraries :: Python Modules +Classifier:: Topic :: Utilities diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..50feaef --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Sphinx>=1.1 diff --git a/setup.py b/setup.py index 6a158f8..7b115ca 100644 --- a/setup.py +++ b/setup.py @@ -9,29 +9,30 @@ management, encryption and signature functionality from Python programs. \ It is intended for use with Python 2.4 or greater.", license="""Copyright (C) 2008-2012 by Vinay Sajip. All Rights Reserved. See LICENSE for license.""", version=version, - author="Vinay Sajip", - author_email="vinay_sajip@red-dove.com", - maintainer="Vinay Sajip", - maintainer_email="vinay_sajip@red-dove.com", - url="http://packages.python.org/python-gnupg/index.html", + author="Isis Agora Lovecruft", + author_email="isis@leap.se", + maintainer="Isis Agora Lovecruft", + maintainer_email="isis@leap.se", + url="https://github.com/isislovecruft/python-gnupg", py_modules=["gnupg"], - platforms="No particular restrictions", - download_url="http://python-gnupg.googlecode.com/files/python-gnupg-%s.tar.gz" % version, + platforms="Linux, BSD, OSX, Windows", + download_url="https://github.com/isislovecruft/python-gnupg/archive/develop.zip", classifiers=[ - 'Development Status :: 5 - Production/Stable', + 'Development Status :: 4 - Alpha', "Intended Audience :: Developers", - 'License :: OSI Approved :: BSD License', + 'Classifier:: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)', "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 2.4", - "Programming Language :: Python :: 2.5", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.0", "Programming Language :: Python :: 3.1", "Programming Language :: Python :: 3.2", - "Operating System :: OS Independent", - "Topic :: Software Development :: Libraries :: Python Modules" + "Topic :: Software Development :: Libraries :: Python Modules", + 'Classifier:: Topic :: Security :: Cryptography', + 'Classifier:: Topic :: Software Development :: Libraries :: Python Modules', + 'Classifier:: Topic :: Utilities', + ] ) From dbdcc848a5b63141e047613620ace8b1c1a07902 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 9 Apr 2013 13:18:57 +0000 Subject: [PATCH 039/397] Update .gitignore. --- .gitignore | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.gitignore b/.gitignore index f6a314e..5abff24 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,14 @@ keys/* # Ignore distutils record of installed files: installed-files.txt + +# Ignore PyCharm files: +.idea/* + +# and git-tickets and tickets: +.tickets/* +tickets/* + +# Ignore virtualenv folders, if they are here: +include/* +local/* \ No newline at end of file From a54fa33ff561dad433a74671f42293fc0659ff8f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 9 Apr 2013 13:19:27 +0000 Subject: [PATCH 040/397] Bump because minor version because we've rewritten large chunks of the codebase. --- gnupg.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gnupg.py b/gnupg.py index aeb6814..c707c14 100644 --- a/gnupg.py +++ b/gnupg.py @@ -42,7 +42,9 @@ Steve Traugott's documentation: Steve Traugott, stevegt@terraluna.org Thu Jun 23 21:27:20 PDT 2005 + Vinay Sajip's documentation: + This version of the module has been modified from Steve Traugott's version (see http://trac.t7a.org/isconf/browser/trunk/lib/python/isconf/GPG.py) by Vinay Sajip to make use of the subprocess module (Steve's version uses @@ -56,7 +58,7 @@ Vinay Sajip's documentation: """ __module__ = 'gnupg' -__version__ = "0.3.1" +__version__ = "0.4.0" __author__ = "Isis Agora Lovecruft" __date__ = "12 Febuary 2013" From b8ce52963865093659ae39812a0eddf0a794af47 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 18:00:00 +0000 Subject: [PATCH 041/397] Remove duplicate log statement of the GnuPG command we're running. --- gnupg.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gnupg.py b/gnupg.py index c707c14..7b9ab5e 100644 --- a/gnupg.py +++ b/gnupg.py @@ -1215,7 +1215,9 @@ class GPG(object): [cmd.append(opt) for opt in iter(_sanitise_list(self.options))] if args: [cmd.append(arg) for arg in iter(_sanitise_list(args))] - logger.debug("make_args(): Using command: %s" % cmd) + ## so that we don't print it twice, here and in _open_subprocess(): + if not self.verbose: + logger.debug("make_args(): Using command: %s" % cmd) return cmd def _open_subprocess(self, args=None, passphrase=False): From 8c6821ea0ea085e93c9dc3fbd11e6e45c126ac6c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 18:03:52 +0000 Subject: [PATCH 042/397] Remove log statement entirely because otherwise it's triplicate. --- gnupg.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/gnupg.py b/gnupg.py index 7b9ab5e..f92adfd 100644 --- a/gnupg.py +++ b/gnupg.py @@ -1215,9 +1215,6 @@ class GPG(object): [cmd.append(opt) for opt in iter(_sanitise_list(self.options))] if args: [cmd.append(arg) for arg in iter(_sanitise_list(args))] - ## so that we don't print it twice, here and in _open_subprocess(): - if not self.verbose: - logger.debug("make_args(): Using command: %s" % cmd) return cmd def _open_subprocess(self, args=None, passphrase=False): From c96aed31ed361370c0d27daeef1b385a13bde357 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 19:14:48 +0000 Subject: [PATCH 043/397] Add function for getting a string of today's date in %Y-%m-%d format. --- gnupg.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/gnupg.py b/gnupg.py index f92adfd..c61c2d4 100644 --- a/gnupg.py +++ b/gnupg.py @@ -70,6 +70,8 @@ try: except ImportError: from cStringIO import StringIO +from datetime import datetime + import codecs import locale import logging @@ -484,6 +486,11 @@ def _make_binary_stream(s, encoding): rv = StringIO(s) return rv +def _today(): + """Get the current date as a string in the form %Y-%m-%d.""" + now_string = datetime.now().__str__() + return now_string.split(' ', 1)[0] + def _sanitise(*args): """ Take an arg or the key portion of a kwarg and check that it is in the set From 24da4d186c8e460a486409f1bed678a66fe73a72 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 19:15:43 +0000 Subject: [PATCH 044/397] Factor out unnecessary parsing for the presence of the passphrase fd. --- gnupg.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/gnupg.py b/gnupg.py index c61c2d4..386fcaf 100644 --- a/gnupg.py +++ b/gnupg.py @@ -1308,15 +1308,11 @@ class GPG(object): stderr.close() stdout.close() - def _handle_io(self, args, file, result, passphrase=None, binary=False): + def _handle_io(self, args, file, result, passphrase=False, binary=False): """ Handle a call to GPG - pass input data, collect output data. """ - if passphrase is not None: - ask_passphrase = True - else: - ask_passphrase = False - p = self._open_subprocess(args, ask_passphrase) + p = self._open_subprocess(args, passphrase) if not binary: stdin = codecs.getwriter(self.encoding)(p.stdin) else: From dc0d9cf70b6db4ca200145a2e25d8fe4bb3a6bbc Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 19:16:58 +0000 Subject: [PATCH 045/397] Update docstring for GPG.gen_key_input() with details on batch file syntax. --- gnupg.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/gnupg.py b/gnupg.py index 386fcaf..062b1b7 100644 --- a/gnupg.py +++ b/gnupg.py @@ -1620,8 +1620,15 @@ class GPG(object): return result def gen_key_input(self, **kwargs): - """ - Generate --gen-key input per gpg doc/DETAILS + """Generate GnuPG key(s) through batch file key generation. + + The GnuPG batch file key generation feature allows unattended key + generation by creating a file with special syntax and then providing it + to: + $ gpg --gen-key --batch + + see http://www.gnupg.org/documentation/manuals/gnupg-devel/Unattended-GPG-key-generation.html#Unattended-GPG-key-generation + for more details. """ parms = {} for key, val in list(kwargs.items()): From 00e94d6b34590ce982880ca9893f34c9b35df47a Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 19:17:33 +0000 Subject: [PATCH 046/397] Hack around the nasty ctb=%d.02 GnuPG bug that keeps resurfacing. * https://bugs.g10code.com/gnupg/issue1179 * This bug has resurfaced several times in GnuPG's history, and as far as I've understood it, it is due to a generation or an attempt at parsing a .gpg file (a message/file, or one of the keyrings) which is generated/formatted such that the GnuPG data packets comprise a certain number of bytes (562 bytes precisely) and/or the demarcation signal (two equal signs, '==') of the trailing ctb (I assume 'ctb' stands for cipher text bytes; it's everywhere in GnuPG's sourcecode) CRC checksum ends up on its own line, like so: -----BEGIN PGP MESSAGE----- hQIMA1DJjYe0ARAPAQ/6A7PWiyfiBIpbJ7+XbaptYPB0muoi3rXF4cuIXU37uDug 04Cck+8u9hcMjhYd0pN6zJMJLO8CUOdUKJblpxPSYc7yanJDc33n1jsdkr7XNbbz Xjh1z3EJZPhhp3YE7lqgnTTXQsGUihnhwiQqsuNgCVZfSwXpVFa+IHC+/AXhAPLl /BoY6+YOupD8G4bOUCU/XBvQp8uH9kh9n9101+6SopJ94H354X0PtZJs5BISCW7g /tUxAqxLesDaZkHF91HL7AJE8+/pheTc8vHGP+Zu8/Q5Lr+GW2cUrCXXxUV/xRIh P7t7WDdU5V1BXyughEBP7Jwr8G3llKr42ZRVgvOyFIe+sMVibq2dEhxJr/sjR0j6 lYws+tZVWbmuxOnMDCjgIaEverU0nnd1SqQ/tS7yFh8lpBfMay2IdVVh+Bi3zDeT Kb/h3tCjG0++h5FuG8UBWv13LRlfcF817ShnwoG80y4cESgCXxZGkd9DRExH8wy8 VJpIaVKiaZs5jlHLHX4JWy2IVmEvk/6xwZZwrNV4sGYzDGkxHLyTUUQu/NOkCtAb 2Zs9mGrtsSGSoP6lfKDk9hh8fJtPGxGzIezVR9WXYKeYAfGZOzMI1Ji1jF1fO21G KqTaF2WnWfaWmsuICTiBntgNntZRsXwmMNkcBt5LVK8uRpWQqIQmAkN4vl6SgovS QAHrC6D9mUmoNvKcS71aH4uZjkfYtNmgp8i5cmTP6UaGouVeqzDCtK/vIy1V/iaX 89vcdsILlMMVB/10Rtscyus ==suET -----END PGP MESSAGE----- When this happens, GnuPG *stops* parsing entirely before the CRC checksum following the '==', and thus the next time it starts parsing again the first thing it gets is the end header '-----END PGP MESSAGE-----'. * My solution for hacking around this is to export directly to a keyring, which can either be one we create for this purpose, or the keyring set as self.pubring. I do not know yet if this will affect --list-keys. --- gnupg.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gnupg.py b/gnupg.py index 062b1b7..dcb571a 100644 --- a/gnupg.py +++ b/gnupg.py @@ -1636,7 +1636,7 @@ class GPG(object): if str(val).strip(): # skip empty strings parms[key] = val parms.setdefault('Key-Type', 'RSA') - parms.setdefault('Key-Length', 2048) + parms.setdefault('Key-Length', 4096) parms.setdefault('Name-Real', "Autogenerated Key") parms.setdefault('Name-Comment', "Generated by gnupg.py") try: @@ -1649,6 +1649,8 @@ class GPG(object): out = "Key-Type: %s\n" % parms.pop('Key-Type') for key, val in list(parms.items()): out += "%s: %s\n" % (key, val) + out += "%%pubring %s.pub\n" % self.pubring + out += "%%secring %s.pub\n" % self.secring out += "%commit\n" return out From d861b36305192ad5fe7a6439dc28d841a895d0f0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 21:32:10 +0000 Subject: [PATCH 047/397] Fix #2162 add stream check. --- gnupg.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/gnupg.py b/gnupg.py index dcb571a..38cc90e 100644 --- a/gnupg.py +++ b/gnupg.py @@ -116,12 +116,12 @@ def _copy_data(instream, outstream): Copy data from one stream to another. :param instream: A file descriptor to read from. - :param outstream: A file descriptor to write to. + :param outstream: The file descriptor of a tmpfile to write to. """ sent = 0 try: - assert isinstance(instream, BytesIO), "instream is not a file" + assert _is_stream(instream), "instream is not a stream" assert isinstance(outstream, file), "outstream is not a file" except AssertionError as ae: logger.exception(ae) @@ -469,6 +469,15 @@ def _is_file(input): else: return True +def _is_stream(input): + """Check that the input is a byte stream. + + :param input: An object provided for reading from or writing to + :rtype: C{bool} + :returns: True if :param:`input` is a stream, False if otherwise. + """ + return isinstance(input, BytesIO) + def _is_sequence(instance): return isinstance(instance,list) or isinstance(instance,tuple) From 0ce277a3670b373c4513078e90b183a8ae679560 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 21:33:10 +0000 Subject: [PATCH 048/397] Add options '--list-secret-keys' and '--fixed-list-mode' to _is_allowed(). --- gnupg.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gnupg.py b/gnupg.py index 38cc90e..fbb66c2 100644 --- a/gnupg.py +++ b/gnupg.py @@ -410,7 +410,8 @@ def _is_allowed(input): ## eg, --no-show-photos would mitigate things like ## https://www-01.ibm.com/support/docview.wss?uid=swg21620982 _allowed = frozenset( - ['--list-keys', '--list-packets', '--with-colons', + ['--list-keys', '--list-secret-keys', '--fixed-list-mode', + '--list-packets', '--with-colons', '--delete-keys', '--delete-secret-keys', '--encrypt', '--encrypt-files', '--print-mds', '--print-md', '--sign', From 582a17beb8b1b8612cd9fde1551dd680e42623e6 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 21:34:06 +0000 Subject: [PATCH 049/397] Remove a comment about pre-Python2.6 compatibility, since we don't care. --- gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg.py b/gnupg.py index fbb66c2..96ff62b 100644 --- a/gnupg.py +++ b/gnupg.py @@ -429,7 +429,7 @@ def _is_allowed(input): assert _allowed.issubset(_possible), \ '_allowed is not subset of known options, difference: %s' \ % _allowed.difference(_possible) - except AssertionError as ae: ## 'as' syntax requires python>=2.6 + except AssertionError as ae: logger.debug("gnupg._is_allowed(): %s" % ae.message) raise UsageError(ae.message) From dd25c3742af220953156f41ab233406265879d7d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 21:37:03 +0000 Subject: [PATCH 050/397] Remove some extraneous log statements, make others more explicit. --- gnupg.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/gnupg.py b/gnupg.py index 96ff62b..85fb9f3 100644 --- a/gnupg.py +++ b/gnupg.py @@ -448,7 +448,7 @@ def _is_allowed(input): try: assert hyphenated in _allowed except AssertionError as ae: - logger.warn("Dropping option '%s'..." + logger.warn("_is_allowed(): Dropping option '%s'..." % _fix_unsafe(hyphenated)) raise ProtectedOption("Option '%s' not supported." % _fix_unsafe(hyphenated)) @@ -555,8 +555,7 @@ def _sanitise(*args): assert allowed_flag is not None, \ "_check_arg_and_value(): got None for allowed_flag" except (AssertionError, ProtectedOption) as error: - logger.warn(error.message) - logger.debug("Dropping option '%s'..." % _fix_unsafe(arg)) + logger.warn("_sanitise(): %s" % error.message) else: safe_values += (allowed_flag + " ") if isinstance(value, str): @@ -1117,17 +1116,19 @@ class GPG(object): :returns: """ + logger.warn("") + if not gpghome: gpghome = os.path.join(os.getcwd(), 'gnupg') self.gpghome = _fix_unsafe(gpghome) if self.gpghome: if not os.path.isdir(self.gpghome): message = ("Creating gpg home dir: %s" % gpghome) - logger.debug("GPG.__init__(): %s" % message) + logger.warn("GPG.__init__(): %s" % message) os.makedirs(self.gpghome, 0x1C0) if not os.path.isabs(self.gpghome): message = ("Got non-abs gpg home dir path: %s" % self.gpghome) - logger.debug("GPG.__init__(): %s" % message) + logger.warn("GPG.__init__(): %s" % message) self.gpghome = os.path.abspath(self.gpghome) else: message = ("Unsuitable gpg home dir: %s" % gpghome) From 38cc8134122f01472d248e7e17b3e89ba777d9aa Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 21:38:11 +0000 Subject: [PATCH 051/397] Rearrange the logic on checking safe_values so that None is skipped. --- gnupg.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/gnupg.py b/gnupg.py index 85fb9f3..164f076 100644 --- a/gnupg.py +++ b/gnupg.py @@ -562,18 +562,22 @@ def _sanitise(*args): value_list = value.split(' ') for value in value_list: safe_value = _fix_unsafe(value) - if allowed_flag == '--encrypt' or '--encrypt-files' \ - or '--decrypt' or '--decrypt-file' \ - or '--import' or '--verify': + if safe_value is not None and not safe_value.strip() == "": + if allowed_flag in ['--encrypt', '--encrypt-files', + '--decrypt', '--decrypt-file', + '--import', '--verify']: ## Place checks here: - if not safe_value == "" and _is_file(safe_value): - safe_values += (safe_value + " ") + if _is_file(safe_value): + safe_values += (safe_value + " ") + else: + logger.debug( + "_sanitize(): Got non-file for %s option: %s" + % (allowed_flag, safe_value)) else: - logger.debug("Got non-filename for %s option: %s" - % (allowed_flag, safe_value)) - else: - safe_values += (safe_value + " ") - logger.debug("Got non-checked value: %s" % safe_value) + safe_values += (safe_value + " ") + logger.debug( + "_sanitize(): No configured checks for value: %s" + % safe_value) return safe_values checked = [] @@ -599,7 +603,7 @@ def _sanitise(*args): logger.debug("Got non-flag argument: %s" % filo[0]) filo.pop() safe = _check_arg_and_value(new_arg, new_value) - if safe is not None and safe.strip() != '': + if safe is not None and not safe.strip() == '': logger.debug("_sanitise(): appending args: %s" % safe) checked.append(safe) else: From febabccbdade3430fbeb8f9e06b9f9127ed4773f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 21:43:54 +0000 Subject: [PATCH 052/397] Pep8'ing the docstrings again. --- test_gnupg.py | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/test_gnupg.py b/test_gnupg.py index 88a7dcd..3540902 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -184,15 +184,11 @@ class GPGTestCase(unittest.TestCase): self.assertGreater(result.find(expected4), 0) def test_gpg_binary_not_abs(self): - """ - Test that a non-absolute path to gpg results in a full path. - """ + """Test that a non-absolute path to gpg results in a full path.""" self.assertTrue(os.path.isabs(self.gpg.gpgbinary)) def test_make_args_drop_protected_options(self): - """ - Test that unsupported gpg options are dropped. - """ + """Test that unsupported gpg options are dropped.""" self.gpg.options = ['--tyrannosaurus-rex', '--stegosaurus'] self.gpg.keyring = self.secring cmd = self.gpg.make_args(None, False) @@ -203,9 +199,7 @@ class GPGTestCase(unittest.TestCase): self.assertListEqual(cmd, expected) def test_make_args(self): - """ - Test argument line construction. - """ + """Test argument line construction.""" not_allowed = ['--bicycle', '--zeppelin', 'train', 'flying-carpet'] self.gpg.options = not_allowed[:-2] args = self.gpg.make_args(not_allowed[2:], False) @@ -214,18 +208,14 @@ class GPGTestCase(unittest.TestCase): self.assertNotIn(na, args) def test_list_keys_initial_public(self): - """ - Test that initially there are no public keys. - """ + """Test that initially there are no public keys.""" public_keys = self.gpg.list_keys() self.assertTrue(is_list_with_len(public_keys, 0), "Empty list expected...got instead: %s" % str(public_keys)) def test_list_keys_initial_secret(self): - """ - Test that initially there are no secret keys. - """ + """Test that initially there are no secret keys.""" private_keys = self.gpg.list_keys(secret=True) self.assertTrue(is_list_with_len(private_keys, 0), "Empty list expected...got instead: %s" @@ -244,9 +234,7 @@ class GPGTestCase(unittest.TestCase): def generate_key_input(self, real_name, email_domain, key_length=None, key_type=None, subkey_type=None, passphrase=None): - """ - Generate a GnuPG batch file for key unattended key creation. - """ + """Generate a GnuPG batch file for key unattended key creation.""" name = real_name.lower().replace(' ', '') ## XXX will GPG just use it's defaults? does it have defaults if @@ -271,16 +259,12 @@ class GPGTestCase(unittest.TestCase): return key_input def generate_key(self, real_name, email_domain, **kwargs): - """ - Generate a basic key. - """ + """Generate a basic key.""" key_input = self.generate_key_input(real_name, email_domain, **kwargs) key = self.gpg.gen_key(key_input) def test_gen_key_input(self): - """ - Test that GnuPG batch file creation is successful. - """ + """Test that GnuPG batch file creation is successful.""" key_input = self.generate_key_input("Francisco Ferrer", "an.ok") From c47968c47a1ec6a49af5da1251797963eb991106 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 21:44:22 +0000 Subject: [PATCH 053/397] Fix unittest to check for secret keyring. --- test_gnupg.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test_gnupg.py b/test_gnupg.py index 3540902..baf5351 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -195,7 +195,8 @@ class GPGTestCase(unittest.TestCase): expected = ['/usr/bin/gpg', '--status-fd 2 --no-tty', '--homedir "%s"' % os.path.join(os.getcwd(), 'keys'), - '--no-default-keyring --keyring "%s"' % self.secring] + '--no-default-keyring --keyring %s --secret-keyring %s' + % (self.pubring, self.secring)] self.assertListEqual(cmd, expected) def test_make_args(self): From a873c1172aa3271b96da5ed7e2b7da1239cc63ed Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 21:44:59 +0000 Subject: [PATCH 054/397] Default the key length for batch files to 4096. --- test_gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_gnupg.py b/test_gnupg.py index baf5351..0addfba 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -241,14 +241,14 @@ class GPGTestCase(unittest.TestCase): ## XXX will GPG just use it's defaults? does it have defaults if ## we've just given it a homedir without a gpg.conf? key_type = 'RSA'if key_type is None else key_type - key_length = 2048 if key_length is None else key_length + key_length = 4096 if key_length is None else key_length batch = {'Key-Type': key_type, 'Key-Length': key_length, 'Name-Comment': 'python-gnupg tester', 'Expire-Date': 1, 'Name-Real': '%s' % real_name, - 'Name-Email': ("%s@%s" % (name, email_domain)) } + 'Name-Email': ("%s@%s" % (name, email_domain))} batch['Passphrase'] = name if passphrase is None else passphrase From 0fcf9676b3cd15caf78b86671b4d4505e71bb7a4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 21:45:36 +0000 Subject: [PATCH 055/397] Add checks to test_gen_key_input(). --- test_gnupg.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test_gnupg.py b/test_gnupg.py index 0addfba..85e7e76 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -267,7 +267,8 @@ class GPGTestCase(unittest.TestCase): def test_gen_key_input(self): """Test that GnuPG batch file creation is successful.""" key_input = self.generate_key_input("Francisco Ferrer", "an.ok") - + self.assertIsNotNone(key_input) + self.assertGreater(key_input.find('Francisco Ferrer'), 0) def test_rsa_key_generation(self): """ From 32c6c1b3edd2afdd952cc356e49da8b98ce9b661 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 21:46:06 +0000 Subject: [PATCH 056/397] Lower key length and fix expiry date in test_invalid_key_type(). --- test_gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_gnupg.py b/test_gnupg.py index 85e7e76..cbcbec8 100644 --- a/test_gnupg.py +++ b/test_gnupg.py @@ -328,9 +328,9 @@ class GPGTestCase(unittest.TestCase): 'Key-Type': 'INVALID', 'Key-Length': 1024, 'Subkey-Type': 'ELG-E', - 'Subkey-Length': 2048, + 'Subkey-Length': 1024, 'Name-Comment': 'A test user', - 'Expire-Date': self.expire_today(), + 'Expire-Date': 1, 'Name-Real': 'Test Name', 'Name-Email': 'test.name@example.com', } From 0ae817e3b831b80c8d00121cc7a9245edb0e8bc0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 21:56:55 +0000 Subject: [PATCH 057/397] Update _sanitise() docstring with information on return object. --- gnupg.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gnupg.py b/gnupg.py index 164f076..d006452 100644 --- a/gnupg.py +++ b/gnupg.py @@ -545,8 +545,10 @@ def _sanitise(*args): --recipient isis@leap.se" to gpg, then "--encrypt" would be an arg without a value, and "--recipient" would also be an arg, with a value of "isis@leap.se". - :type sanitised: C{str} - :ivar sanitised: The sanitised, allowed options. + :type checked: C{list} + :ivar checked: The sanitised, allowed options and values. + :rtype: C{str} + :returns: A string of the items in :ivar:`checked` delimited by spaces. """ safe_values = str() From 58de4c6c21dd1b09c128c0cd06e6e732ca902566 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 21:58:56 +0000 Subject: [PATCH 058/397] Make a logger statement more explicit. --- gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg.py b/gnupg.py index d006452..f74f998 100644 --- a/gnupg.py +++ b/gnupg.py @@ -645,7 +645,7 @@ def _sanitise_list(arg_list): def _threaded_copy_data(instream, outstream): wr = threading.Thread(target=_copy_data, args=(instream, outstream)) wr.setDaemon(True) - logger.debug('data copier: %r, %r, %r', wr, instream, outstream) + logger.debug('_threaded_copy_data(): %r, %r, %r', wr, instream, outstream) wr.start() return wr From f65618bd83e06f4be69e4768d2e526b64f83a4e2 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 10 Apr 2013 22:44:10 +0000 Subject: [PATCH 059/397] Remove unused function _underscore(). --- gnupg.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/gnupg.py b/gnupg.py index f74f998..298ca57 100644 --- a/gnupg.py +++ b/gnupg.py @@ -649,25 +649,6 @@ def _threaded_copy_data(instream, outstream): wr.start() return wr -def _underscore(input, remove_prefix=False): - """ - Change hyphens to underscores so that GPG option names can be easily - tranlated to object attributes. - - :type input: C{str} - :param input: The input intended for the gnupg process. - - :type remove_prefix: C{bool} - :param remove_prefix: If True, strip leading hyphens from the input. - - :rtype: C{str} - :return: The :param:input with hyphens changed to underscores. - """ - if not remove_prefix: - return input.replace('-', '_') - else: - return input.lstrip('-').replace('-', '_') - def _which(executable, flags=os.X_OK): """Borrowed from Twisted's :mod:twisted.python.proutils . From f01668fe48a86ed6c3a8b9bf38c1bc6055f51564 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 11 Apr 2013 18:16:05 +0000 Subject: [PATCH 060/397] Add NOTES file to docs/ directory. --- docs/NOTES-python-gnupg-3.1-audit.gpg | Bin 0 -> 4534 bytes docs/NOTES-python-openpgp-implementations.gpg | Bin 0 -> 1291 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/NOTES-python-gnupg-3.1-audit.gpg create mode 100644 docs/NOTES-python-openpgp-implementations.gpg diff --git a/docs/NOTES-python-gnupg-3.1-audit.gpg b/docs/NOTES-python-gnupg-3.1-audit.gpg new file mode 100644 index 0000000000000000000000000000000000000000..3973e258b703216f403c6a56815c3e2cd69f050f GIT binary patch literal 4534 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 UX9Z211f!bt2}*oFr7V5}-MHW4bN~PV literal 0 HcmV?d00001 diff --git a/docs/NOTES-python-openpgp-implementations.gpg b/docs/NOTES-python-openpgp-implementations.gpg new file mode 100644 index 0000000000000000000000000000000000000000..0a6e020af7a5b54bc405bf6dd16ff856703b5d6c GIT binary patch literal 1291 zcmV+m1@!ub0t^FC$&H7!0T2%X5C3T(2tI*xUR^Y~kXKP6Whf49ikz+yphJ$MRUI6= zui(-{8;)P`scmE3)LuFtLD+>!|E0a8O`aLnrS7%msmf#JON>lO%2|J&&@In@mna?~ z4#wRFEv-I~v6rk0arelxFz`woPh8ZnQxUs8PJW->2o3hnwiU^|M8KQcMp%Acs!2Da z_7cvmX{ru*=?f_+0O>jcI=H%alja^Qm^qH_Y3WHJv=o5zVLIlWl3XkAGFk|K`xOxo zY*<&_SVG$!Gi^%oeAwer0%!))KW;9G`<<9i9!$|(OKW{2-hz!%eOiL)bxNwV%4;{$ zu3FtYtAgOJmpf)(U*HJU_ffsorJEG}f{ux;auk~nbe|PRHLNRs?WxV(-Axf&3*z3~ zQ5E6M7p`-avnZo@oPy{$`c@2WTQ&v|{^eX6Xe>MqP4CcpE_$uZ5{# zz?w3|5}HFsmCse20Uy7_K;pu%maKp{T8Vx*)I}bK8FWFnDS(U_tl3#{n5d!g8EA7Y z0nfI!bgts(J2|7s=f-5#Dc7oQp_hLjeqkj_E0k|7?)=4?trkl)h{oCU%h-np{%x~@ z)$|%2+EMzm?{Y&11eWbhAi-@*J`nFO{d>`v!Hocb3Mi)Ek)=i!?uRbX=;FN3M zJog!H$q^ml5e$pAF>0onDG}pU@D-6IzQ{JA7h^OEEC}2Ng7F#=xDmc8x%g>xd1fD5?%gK`7DKKn=<2q}%Xj&erz)e1)^~AVxHc8dtj!4h zLxZ?_L2z;n?&hBCxUWL&LUh!GwRUIi4bLSQH~K<)$b(;?zC11TfZeY#73f7x;4Wp| zj#C5eg~%)49S4`A1gbasCAIN;}=JI2(Zq>6mq}ywMcDet{rt@;X{fzU z;!%2EOg;y|k_8@%f#C1kex{chOr>LC<$ZCxbNP_9;eu%zEHfo^wNJZ$Gfr?T?^fTF Bc_07) literal 0 HcmV?d00001 From f68d01b9f2a2f2c609924a2751393dc229c6a428 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 11 Apr 2013 18:21:32 +0000 Subject: [PATCH 061/397] Update the README. --- README.md | 46 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index aa46ee7..1d70df3 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # python-gnupg # ================ -Fork of python-gnupg-0.3.2, patched to remove Popen([...], shell=True). +Fork of python-gnupg-0.3.2, patched to remove ```Popen([...], shell=True)```. ### Installation ### --------------------- + +#### From this git repository #### To install this package from this git repository, do: ``` @@ -19,15 +20,44 @@ Optionally to build the documentation after installation, do: make docs ``` -To get started using python-gnupg's API, see the documentation online at [XXX -FIXME add readthedocs link](), and import the module like so: +To get started using python-gnupg's API, see the [online documentation](https://python-gnupg.readthedocs.org/en/latest/), +and import the module like so: ``` >>> import gnupg ``` +The primary interface class you'll likely want to interact with is +[```gnupg.GPG```](https://python-gnupg.readthedocs.org/en/latest/gnupg.html#gpg): +``` +>>> gpg = gnupg.GPG(gpgbinary='/usr/bin/gpg', +... gpghome='./keys', +... pubring='pubring.gpg', +... secring='secring.gpg') +>>> batch_key_input = gpg.gen_key_input() +>>> print batch_key_input +Key-Type: RSA +Name-Email: isis@wintermute +Name-Comment: Generated by gnupg.py +Key-Length: 4096 +Name-Real: Autogenerated Key +%pubring /home/isis/code/python-gnupg/keys/pubring.gpg +%secring /home/isis/code/python-gnupg/keys/secring.gpg +%commit -To install this package from a tarballed source distribution, do the following: +>>> key = gpg.gen_key(batch_key_input) +>>> print key.fingerprint +245D8FA30F543B742053949F553C0E154F2E7A98 -1. Extract all the files in the distribution archive to some directory on your system. -2. In that directory, run "python setup.py install". -3. Optionally, run "python test_gnupg.py" to ensure that the package is working as expected. +``` + +#### From PyPI #### +Hold your horses, boy. I haven't finished development, so the packages on +[PyPI](https://pypi.python.org) are still the old versions belonging to the +other authors. + +### Bug Reports & Feature Requests ### +Our bugtracker is [here](https://leap.se/code/projects/eip_server/issue/new). + +Please use that for bug reports and feature requests instead of github's +tracker. We're using github for code commenting and review between +collaborators. From 64fd83bf85d34bbfb63275ad5a08d0a5992b3acd Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 11 Apr 2013 18:37:16 +0000 Subject: [PATCH 062/397] Add bug reporter. --- report-a-bug | 260 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 report-a-bug diff --git a/report-a-bug b/report-a-bug new file mode 100644 index 0000000..73e7b98 --- /dev/null +++ b/report-a-bug @@ -0,0 +1,260 @@ +#!/usr/bin/env python +#-*- coding: utf-8 -*- +''' + report-a-bug + ------------ + File a bug against python-gnupg. + + :authors: Wade Leftwich, + Will Holcomb, + Isis Lovecruft, + :license: AGPLv3, see LICENSE and COPYRIGHT files + :copyright: Ā© 2002-2013 Wade Leftwich + Ā© 2006 Will Holcomb + Ā© 2013 Isis Agora Lovecruft + :date: 11 April 2013 + :version: 0.0.1 + +''' + +from __future__ import print_function +from cStringIO import StringIO +from datetime import datetime + +import cookielib +import mimetools +import mimetypes +#import httplib +import os +import stat +import tempfile +import urllib +import urllib2 + +# Controls how sequences are uncoded. If true, elements may be given multiple +# values by assigning a sequence. +doseq = 1 + +def _has_py3k(): + """Check if we're running on Python>=3.0.""" + try: + unicode + return False + except NameError: + return True + +## patch the stupid Python2.x input() problem: +if not _has_py3k(): + input = raw_input + +def _today(): + """Get the current date as a string in the form %Y-%m-%d.""" + now_string = datetime.now().__str__() + return now_string.split(' ', 1)[0] + +def _create_upload_list(): + """Create a dictionary containing information about files to upload.""" + + upload_list = list() + + WANT_UPLOAD = True + FILE_NUMBER = 1 + FILENAME_FIELD = 'attachments[' + str(FILE_NUMBER) + '][file]' + FILEDESC_FIELD = 'attachments[' + str(FILE_NUMBER) + '][description]' + + while WANT_UPLOAD: + do_upload = input("Would you like to attach a file to this ticket? " + + "(y/N) ") + if do_upload.strip() == "": + WANT_UPLOAD = False + break + else: + WANT_UPLOAD = True + + upload = input("Please specify the file to upload, as a filesystem " + + "absolute path or as relative to this directory (%s): " + % os.getcwd()).strip() + + if len(upload) > 0: + if upload.startswith('~'): + upload = os.path.expanduser(upload) + if not os.path.isabs(upload): + upload = os.path.abspath(upload) + try: + assert os.path.isfile(upload), "is not a file" + except AssertionError as ae: + print("Skipping: '%s' %s" % (upload, ae.message)) + else: + upload_fields = {'fields': [FILENAME_FIELD, FILEDESC_FIELD], + 'filepath': upload,} + upload_list.append(upload_fields) + FILE_NUMBER += 1 + return upload_list + +def _create_fields_and_headers(host, url, assign_to=None, + category=None, target_version=None): + REPO_NAME = os.getcwd().rsplit(os.path.sep, 1)[1] + + subject = input("Please provide a brief subject line for the ticket: ") + subject = REPO_NAME + ": " + subject + descript = input("Ticket description:\n ") + whatisit = input("Is this a feature request or a bug report? " + + "(1=feature, 2=bug) ") + if whatisit not in ['1', '2']: + whatisit = '2' + serious = input("How important is this? (1=important, 2=normal, 3=trivial) ") + if serious not in ['1', '2', '3']: + serious = '2' + + headers = { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Encoding': 'gzip, deflate', + 'Accept-Language': 'en-us,en-gb;q=0.9,en;', + 'Connection': 'keep-alive', + 'DNT': '1', + 'Host': host, + 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; rv:10.0) Gecko/20100101 Firefox/10.0', } + + fields = { + 'issue[tracker_id]': whatisit, + 'issue[subject]': subject, + 'issue[description]': descript, + 'issue[status_id]': '2', + 'issue[priority_id]': serious, + 'issue[assigned_to_id]': assign_to, + 'issue[category_id]': category, + 'issue[fixed_version_id]': target_version, + 'issue[start_date]': _today(), + 'issue[due_date]': '', + 'issue[estimated_hours]': '', + 'issue[done_ratio]': '0', + 'issue[custom_field_values][3]': '', + 'issue[custom_field_values][4]': '', + 'issue[custom_field_values][5]': '0', + 'send_notification': '0', + 'send_notification': '1', + 'commit': 'Create', } + + return fields, headers + + +class Callable: + def __init__(self, anycallable): + self.__call__ = anycallable + +class MultipartPostHandler(urllib2.BaseHandler): + handler_order = urllib2.HTTPHandler.handler_order - 10 # needs to run first + + def http_request(self, request): + data = request.get_data() + if data is not None and type(data) != str: + v_files = [] + v_vars = [] + try: + for (key, value) in data.items(): + if type(value) == file: + v_files.append((key, value)) + else: + v_vars.append((key, value)) + except TypeError: + systype, value, traceback = sys.exc_info() + raise TypeError("not non-string sequence or mapping object\n%s" + % traceback) + + if len(v_files) == 0: + data = urllib.urlencode(v_vars, doseq) + else: + boundary, data = self.multipart_encode(v_vars, v_files) + contenttype = 'multipart/form-data; boundary=%s' % boundary + if (request.has_header('Content-Type') and request.get_header( + 'Content-Type').find('multipart/form-data') != 0): + print("Replacing %s with %s" + % (request.get_header('content-type'), + 'multipart/form-data')) + request.add_unredirected_header('Content-Type', contenttype) + request.add_data(data) + return request + + def multipart_encode(self, fields=None, upload_list=None, + boundary=None, buf=None): + if fields is None: + fields = self.fields + if upload_list is None: + upload_list = self.upload_list + if boundary is None: + boundary = mimetools.choose_boundary() + if buf is None: + buf = StringIO() + + for (key, value) in fields: + buf.write('--%s\r\n' % boundary) + buf.write('Content-Disposition: form-data; name="%s"' % key) + buf.write('\r\n\r\n' + value + '\r\n') + + if isinstance(upload_list, list) and len(upload_list) > 0: + for upload in upload_list: + for (name, filepath) in upload: + with open(filepath) as fd: + file_size = os.fstat(fd.fileno())[stat.ST_SIZE] + filename = fd.name.split('/')[-1] + contenttype = mimetypes.guess_type(filename)[0] \ + or 'application/octet-stream' + buf.write('--%s\r\n' % boundary) + buf.write('Content-Disposition: form-data; ') + buf.write('name="%s"; filename="%s"' + % (name[0], filename)) + buf.write('Content-Type: %s\r\n' % contenttype) + # buf.write('Content-Length: %s\r\n' % file_size) + fd.seek(0) + buf.write('\r\n' + fd.read() + '\r\n') + buf.write('--' + boundary + '--\r\n\r\n') + buf.write('--%s\r\n' % boundary) + buf.write('Content-Disposition: form-data; ') + buf.write('name="%s"; filename="%s"' + % (name[1], filename)) + buf.write('\r\n\r\n') + buf.write('--' + boundary + '--\r\n\r\n') + + buf = buf.getvalue() + return boundary, buf + multipart_encode = Callable(multipart_encode) + + https_request = http_request + + +if __name__ == "__main__": + + raise SystemExit("Please fix me! This script needs a login handler.\n" + + "Everything else is finished.") + + ## if you're reusing this script please change these! + host = 'leap.se' + selector = '/code/projects/eip-server/issues/new' + assign_to = '30' ## isis + category = '26' ## email + target_version = '29' ## the close future + + url = 'https://' + host + selector + fields, headers = _create_fields_and_headers(host, selector, assign_to, + category, target_version) + upload_list = _create_upload_list() + cookies = cookielib.CookieJar() + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), + MultipartPostHandler()) + urllib2.install_opener(opener) + + temp = tempfile.mkstemp(suffix=".html") + temp1 = tempfile.mkstemp(suffix=".html") + os.write(temp[0], opener.open('https://'+host+'/code/login', + {'username': 'cypherpunks', + 'password': 'writecode'}).read()) + + for index,upload in enumerate(upload_list): + for (field, filepath) in upload: + fields['file'+'-'+index] = open(filepath, 'rb') + res = opener.open(url, fields) + print("Posted to: %s" % res.geturl()) + print("Server response: %s " % res.code) + print(res.info()) + print(res.read()) + os.remove(temp[1]) From 59951c7486491280cd6ccae7840fcbfe8c68fa40 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 11 Apr 2013 19:11:12 +0000 Subject: [PATCH 063/397] Fix potential MRO conflict in Crypt class. --- gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg.py b/gnupg.py index 298ca57..5768c5d 100644 --- a/gnupg.py +++ b/gnupg.py @@ -931,7 +931,7 @@ class ListKeys(list): class Crypt(Verify): """Handle status messages for --encrypt and --decrypt""" def __init__(self, gpg): - Verify.__init__(self, gpg) + super(Crypt, self).__init__(self, gpg) self.data = '' self.ok = False self.status = '' From e02cfc494607a7d7af5cc52c3c04c9e5a53ef314 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 11 Apr 2013 23:42:15 +0000 Subject: [PATCH 064/397] Update setup.py with new directory structure and updated tags. --- setup.py | 66 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/setup.py b/setup.py index 7b115ca..7248a0c 100644 --- a/setup.py +++ b/setup.py @@ -1,38 +1,38 @@ from distutils.core import setup -from gnupg import __version__ as version +from gnupg.gnupg import __version__ as version +from gnupg.gnupg import __author__ as author setup(name = "python-gnupg", - description="A wrapper for the Gnu Privacy Guard (GPG or GnuPG)", - long_description = "This module allows easy access to GnuPG's key \ + description="A wrapper for the Gnu Privacy Guard (GPG or GnuPG)", + long_description = "This module allows easy access to GnuPG's key \ management, encryption and signature functionality from Python programs. \ -It is intended for use with Python 2.4 or greater.", - license="""Copyright (C) 2008-2012 by Vinay Sajip. All Rights Reserved. See LICENSE for license.""", - version=version, - author="Isis Agora Lovecruft", - author_email="isis@leap.se", - maintainer="Isis Agora Lovecruft", - maintainer_email="isis@leap.se", - url="https://github.com/isislovecruft/python-gnupg", - py_modules=["gnupg"], - platforms="Linux, BSD, OSX, Windows", - download_url="https://github.com/isislovecruft/python-gnupg/archive/develop.zip", - classifiers=[ - 'Development Status :: 4 - Alpha', - "Intended Audience :: Developers", - 'Classifier:: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)', - "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 2.6", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.0", - "Programming Language :: Python :: 3.1", - "Programming Language :: Python :: 3.2", - "Topic :: Software Development :: Libraries :: Python Modules", - 'Classifier:: Topic :: Security :: Cryptography', - 'Classifier:: Topic :: Software Development :: Libraries :: Python Modules', - 'Classifier:: Topic :: Utilities', - - ] -) +It is intended for use with Python 2.6 or greater.", + license="""Copyright Ā© 2013 Isis Lovecruft, et.al. see LICENSE file.""", + version=version, + author=author, + author_email="isis@leap.se", + maintainer="Isis Agora Lovecruft", + maintainer_email="isis@leap.se", + url="https://github.com/isislovecruft/python-gnupg", + packages_dir={'': 'gnupg'}, + packages=[''], + platforms="Linux, BSD, OSX, Windows", + download_url="https://github.com/isislovecruft/python-gnupg/archive/develop.zip", + classifiers=[ + 'Development Status :: 4 - Alpha', + "Intended Audience :: Developers", + 'Classifier:: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)', + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.0", + "Programming Language :: Python :: 3.1", + "Programming Language :: Python :: 3.2", + "Topic :: Software Development :: Libraries :: Python Modules", + 'Classifier:: Topic :: Security :: Cryptography', + 'Classifier:: Topic :: Software Development :: Libraries :: Python Modules', + 'Classifier:: Topic :: Utilities',] + ) From f318573e84398a8070ae505710cb179bcf73468e Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 11 Apr 2013 23:43:02 +0000 Subject: [PATCH 065/397] Removing the TODO list because we killed it. --- TODO.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 3ea950b..0000000 --- a/TODO.md +++ /dev/null @@ -1 +0,0 @@ -- [ ] Separate into separate files, having one module file with everything and nearly 2000 LOC is a little bit unwieldy. From d97ae0b356adcb34b8e281ad97f0f757d14326f9 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 11 Apr 2013 23:45:22 +0000 Subject: [PATCH 066/397] Relayout module structure. --- gnupg.py => gnupg/gnupg.py | 824 +------------------- gnupg/parsers.py | 833 +++++++++++++++++++++ test_gnupg.py => gnupg/tests/test_gnupg.py | 0 util.py => gnupg/util.py | 0 4 files changed, 859 insertions(+), 798 deletions(-) rename gnupg.py => gnupg/gnupg.py (53%) create mode 100644 gnupg/parsers.py rename test_gnupg.py => gnupg/tests/test_gnupg.py (100%) rename util.py => gnupg/util.py (100%) diff --git a/gnupg.py b/gnupg/gnupg.py similarity index 53% rename from gnupg.py rename to gnupg/gnupg.py index 5768c5d..cdfae42 100644 --- a/gnupg.py +++ b/gnupg/gnupg.py @@ -1,5 +1,21 @@ #!/usr/bin/env python #-*- encoding: utf-8 -*- +# +# This file is part of python-gnupg, a Python wrapper around GnuPG. +# Copyright Ā© 2013 Isis Lovecruft +# Ā© 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 Affero 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 +# GNU Affero General Public License for more details. """ gnupg.py ======== @@ -19,10 +35,12 @@ presented to gnupg, in order to avoid potential vulnerabilities. :Date: $Date: 2013-04-04 01:11:01 +0000 (Thursday, April 4, 2013) $ :Description: Documentation of python-gnupg, a Python module for GnuPG. -Previous Authors' Documentation: --------------------------------- + +Previous Authors' Documentation +------------------------------- Steve Traugott's documentation: + Portions of this module are derived from A.M. Kuchling's well-designed GPG.py, using Richard Jones' updated version 1.3, which can be found in the pycrypto CVS repository on Sourceforge: @@ -62,8 +80,6 @@ __version__ = "0.4.0" __author__ = "Isis Agora Lovecruft" __date__ = "12 Febuary 2013" -import locale - try: from io import StringIO from io import BytesIO @@ -90,26 +106,19 @@ except ImportError: class NullHandler(logging.Handler): def handle(self, record): pass +finally: + logger = logging.getLogger(__module__) + if not logger.handlers: + logger.addHandler(NullHandler()) + try: unicode _py3k = False except NameError: _py3k = True - ESCAPE_PATTERN = re.compile(r'\\x([0-9a-f][0-9a-f])', re.I) -logger = logging.getLogger(__module__) -if not logger.handlers: - logger.addHandler(NullHandler()) - - -class ProtectedOption(Exception): - """Raised when the option passed to GPG is disallowed.""" - -class UsageError(Exception): - """Raised when incorrect usage of the API occurs..""" - def _copy_data(instream, outstream): """ @@ -158,24 +167,6 @@ def _copy_data(instream, outstream): else: logger.debug("closed output, %d bytes sent", sent) -def _fix_unsafe(input): - """ - Find characters used to escape from a string into a shell, and wrap them - in quotes if they exist. Regex pilfered from python-3.x shlex module. - - :param input: The input intended for the gnupg process. - """ - ## xxx do we want to add ';'? - _unsafe = re.compile(r'[^\w@%+=:,./-]', 256) - try: - if len(_unsafe.findall(input)) == 0: - return input - else: - clean = "'" + input.replace("'", "'\"'\"'") + "'" - return clean - except TypeError: - return None - def _has_readwrite(path): """ Determine if the real uid/gid of the executing user has read and write @@ -189,273 +180,6 @@ def _has_readwrite(path): """ return os.access(path, os.R_OK and os.W_OK) -def _hyphenate(input, add_prefix=False): - """ - Change underscores to hyphens so that object attributes can be easily - tranlated to GPG option names. - - :type input: C{str} - :param input: The attribute to hyphenate. - - :type add_prefix: C{bool} - :param add_prefix: If True, add leading hyphens to the input. - - :rtype: C{str} - :return: The :param:input with underscores changed to hyphens. - """ - ret = '--' if add_prefix else '' - ret += input.replace('_', '-') - return ret - -def _is_allowed(input): - """ - Check that an option or argument given to GPG is in the set of allowed - options, the latter being a strict subset of the set of all options known - to GPG. - - :type input: C{str} - :param input: An input meant to be parsed as an option or flag to the GnuPG - process. Should be formatted the same as an option or flag - to the commandline gpg, i.e. "--encrypt-files". - - :type _possible: C{frozenset} - :ivar _possible: All known GPG options and flags. - - :type _allowed: C{frozenset} - :ivar _allowed: All allowed GPG options and flags, e.g. all GPG options and - flags which we are willing to acknowledge and parse. If we - want to support a new option, it will need to have its own - parsing class and its name will need to be added to this - set. - - :rtype: C{Exception} or C{str} - :raise: UsageError if :ivar:_allowed is not a subset of :ivar:_possible. - ProtectedOption if :param:input is not in the set :ivar:_allowed. - :return: The original parameter :param:input, unmodified and unsanitized, - if no errors occur. - """ - - _all = (""" ---allow-freeform-uid --multifile ---allow-multiple-messages --no ---allow-multisig-verification --no-allow-freeform-uid ---allow-non-selfsigned-uid --no-allow-multiple-messages ---allow-secret-key-import --no-allow-non-selfsigned-uid ---always-trust --no-armor ---armor --no-armour ---armour --no-ask-cert-expire ---ask-cert-expire --no-ask-cert-level ---ask-cert-level --no-ask-sig-expire ---ask-sig-expire --no-auto-check-trustdb ---attribute-fd --no-auto-key-locate ---attribute-file --no-auto-key-retrieve ---auto-check-trustdb --no-batch ---auto-key-locate --no-comments ---auto-key-retrieve --no-default-keyring ---batch --no-default-recipient ---bzip2-compress-level --no-disable-mdc ---bzip2-decompress-lowmem --no-emit-version ---card-edit --no-encrypt-to ---card-status --no-escape-from-lines ---cert-digest-algo --no-expensive-trust-checks ---cert-notation --no-expert ---cert-policy-url --no-force-mdc ---change-pin --no-force-v3-sigs ---charset --no-force-v4-certs ---check-sig --no-for-your-eyes-only ---check-sigs --no-greeting ---check-trustdb --no-groups ---cipher-algo --no-literal ---clearsign --no-mangle-dos-filenames ---command-fd --no-mdc-warning ---command-file --no-options ---comment --no-permission-warning ---completes-needed --no-pgp2 ---compress-algo --no-pgp6 ---compression-algo --no-pgp7 ---compress-keys --no-pgp8 ---compress-level --no-random-seed-file ---compress-sigs --no-require-backsigs ---ctapi-driver --no-require-cross-certification ---dearmor --no-require-secmem ---dearmour --no-rfc2440-text ---debug --no-secmem-warning ---debug-all --no-show-notation ---debug-ccid-driver --no-show-photos ---debug-level --no-show-policy-url ---decrypt --no-sig-cache ---decrypt-files --no-sig-create-check ---default-cert-check-level --no-sk-comments ---default-cert-expire --no-strict ---default-cert-level --notation-data ---default-comment --not-dash-escaped ---default-key --no-textmode ---default-keyserver-url --no-throw-keyid ---default-preference-list --no-throw-keyids ---default-recipient --no-tty ---default-recipient-self --no-use-agent ---default-sig-expire --no-use-embedded-filename ---delete-keys --no-utf8-strings ---delete-secret-and-public-keys --no-verbose ---delete-secret-keys --no-version ---desig-revoke --openpgp ---detach-sign --options ---digest-algo --output ---disable-ccid --override-session-key ---disable-cipher-algo --passphrase ---disable-dsa2 --passphrase-fd ---disable-mdc --passphrase-file ---disable-pubkey-algo --passphrase-repeat ---display --pcsc-driver ---display-charset --personal-cipher-preferences ---dry-run --personal-cipher-prefs ---dump-options --personal-compress-preferences ---edit-key --personal-compress-prefs ---emit-version --personal-digest-preferences ---enable-dsa2 --personal-digest-prefs ---enable-progress-filter --pgp2 ---enable-special-filenames --pgp6 ---enarmor --pgp7 ---enarmour --pgp8 ---encrypt --photo-viewer ---encrypt-files --pipemode ---encrypt-to --preserve-permissions ---escape-from-lines --primary-keyring ---exec-path --print-md ---exit-on-status-write-error --print-mds ---expert --quick-random ---export --quiet ---export-options --reader-port ---export-ownertrust --rebuild-keydb-caches ---export-secret-keys --recipient ---export-secret-subkeys --recv-keys ---fast-import --refresh-keys ---fast-list-mode --remote-user ---fetch-keys --require-backsigs ---fingerprint --require-cross-certification ---fixed-list-mode --require-secmem ---fix-trustdb --rfc1991 ---force-mdc --rfc2440 ---force-ownertrust --rfc2440-text ---force-v3-sigs --rfc4880 ---force-v4-certs --run-as-shm-coprocess ---for-your-eyes-only --s2k-cipher-algo ---gen-key --s2k-count ---gen-prime --s2k-digest-algo ---gen-random --s2k-mode ---gen-revoke --search-keys ---gnupg --secret-keyring ---gpg-agent-info --send-keys ---gpgconf-list --set-filename ---gpgconf-test --set-filesize ---group --set-notation ---help --set-policy-url ---hidden-encrypt-to --show-keyring ---hidden-recipient --show-notation ---homedir --show-photos ---honor-http-proxy --show-policy-url ---ignore-crc-error --show-session-key ---ignore-mdc-error --sig-keyserver-url ---ignore-time-conflict --sign ---ignore-valid-from --sign-key ---import --sig-notation ---import-options --sign-with ---import-ownertrust --sig-policy-url ---interactive --simple-sk-checksum ---keyid-format --sk-comments ---keyring --skip-verify ---keyserver --status-fd ---keyserver-options --status-file ---lc-ctype --store ---lc-messages --strict ---limit-card-insert-tries --symmetric ---list-config --temp-directory ---list-key --textmode ---list-keys --throw-keyid ---list-only --throw-keyids ---list-options --trustdb-name ---list-ownertrust --trusted-key ---list-packets --trust-model ---list-public-keys --try-all-secrets ---list-secret-keys --ttyname ---list-sig --ttytype ---list-sigs --ungroup ---list-trustdb --update-trustdb ---load-extension --use-agent ---local-user --use-embedded-filename ---lock-multiple --user ---lock-never --utf8-strings ---lock-once --verbose ---logger-fd --verify ---logger-file --verify-files ---lsign-key --verify-options ---mangle-dos-filenames --version ---marginals-needed --warranty ---max-cert-depth --with-colons ---max-output --with-fingerprint ---merge-only --with-key-data ---min-cert-level --yes -""").split() - - _possible = frozenset(_all) - - ## these are the allowed options we will handle so far, all others should - ## be dropped. this dance is so that when new options are added later, we - ## merely add the to the _allowed list, and the `` _allowed.issubset`` - ## assertion will check that GPG will recognise them - ## - ## xxx key fetching/retrieving options: [fetch_keys, merge_only, recv_keys] - ## - ## xxx which ones do we want as defaults? - ## eg, --no-show-photos would mitigate things like - ## https://www-01.ibm.com/support/docview.wss?uid=swg21620982 - _allowed = frozenset( - ['--list-keys', '--list-secret-keys', '--fixed-list-mode', - '--list-packets', '--with-colons', - '--delete-keys', '--delete-secret-keys', - '--encrypt', '--encrypt-files', - '--print-mds', '--print-md', '--sign', - '--gen-key', '--batch', - '--decrypt', '--decrypt-files', - '--import', - '--verify', - '--version', - '--status-fd', '--no-tty', '--passphrase-fd', - '--homedir', '--no-default-keyring', '--keyring', '--secret-keyring', - '--fingerprint']) - - ## check that _allowed is a subset of _possible - try: - assert _allowed.issubset(_possible), \ - '_allowed is not subset of known options, difference: %s' \ - % _allowed.difference(_possible) - except AssertionError as ae: - logger.debug("gnupg._is_allowed(): %s" % ae.message) - raise UsageError(ae.message) - - ## if we got a list of args, join them - if not isinstance(input, str): - input = ' '.join([x for x in input]) - - if isinstance(input, str): - if input.find('_') > 0: - if not input.startswith('--'): - hyphenated = _hyphenate(input, add_prefix=True) - else: - hyphenated = _hyphenate(input) - else: - hyphenated = input - try: - assert hyphenated in _allowed - except AssertionError as ae: - logger.warn("_is_allowed(): Dropping option '%s'..." - % _fix_unsafe(hyphenated)) - raise ProtectedOption("Option '%s' not supported." - % _fix_unsafe(hyphenated)) - else: - return input - return None - def _is_file(input): """ Check that the size of the thing which is supposed to be a filename has @@ -501,147 +225,6 @@ def _today(): now_string = datetime.now().__str__() return now_string.split(' ', 1)[0] -def _sanitise(*args): - """ - Take an arg or the key portion of a kwarg and check that it is in the set - of allowed GPG options and flags, and that it has the correct type. Then, - attempt to escape any unsafe characters. If an option is not allowed, - drop it with a logged warning. Returns a dictionary of all sanitised, - allowed options. - - 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 - safety checks added here. - - GnuPG has three-hundred and eighteen commandline flags. Also, not all - implementations of OpenPGP parse PGP packets and headers in the same way, - so there is added potential there for messing with calls to GPG. - - For information on the PGP message format specification, see: - https://www.ietf.org/rfc/rfc1991.txt - - If you're asking, "Is this *really* necessary?": No. Not really. See: - https://xkcd.com/1181/ - - :type args: C{str} - :param args: (optional) The boolean arguments which will be passed to the - GnuPG process. - :rtype: C{str} - :param: :ivar:sanitised - """ - - def _check_arg_and_value(arg, value): - """ - 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 - :ivar:sanitised. - - :type arg: C{str} - - :param arg: The arguments which will be passed to the GnuPG process, - and, optionally their corresponding values. The values are - any additional arguments following the GnuPG option or - flag. For example, if we wanted to pass "--encrypt - --recipient isis@leap.se" to gpg, then "--encrypt" would be - an arg without a value, and "--recipient" would also be an - arg, with a value of "isis@leap.se". - :type checked: C{list} - :ivar checked: The sanitised, allowed options and values. - :rtype: C{str} - :returns: A string of the items in :ivar:`checked` delimited by spaces. - """ - safe_values = str() - - try: - allowed_flag = _is_allowed(arg) - assert allowed_flag is not None, \ - "_check_arg_and_value(): got None for allowed_flag" - except (AssertionError, ProtectedOption) as error: - logger.warn("_sanitise(): %s" % error.message) - else: - safe_values += (allowed_flag + " ") - if isinstance(value, str): - value_list = value.split(' ') - for value in value_list: - safe_value = _fix_unsafe(value) - if safe_value is not None and not safe_value.strip() == "": - if allowed_flag in ['--encrypt', '--encrypt-files', - '--decrypt', '--decrypt-file', - '--import', '--verify']: - ## Place checks here: - if _is_file(safe_value): - safe_values += (safe_value + " ") - else: - logger.debug( - "_sanitize(): Got non-file for %s option: %s" - % (allowed_flag, safe_value)) - else: - safe_values += (safe_value + " ") - logger.debug( - "_sanitize(): No configured checks for value: %s" - % safe_value) - return safe_values - - checked = [] - - if args is not None: - for arg in args: - if isinstance(arg, str): - logger.debug("_sanitise(): Got arg string: %s" % arg) - ## if we're given a string with a bunch of options in it split - ## them up and deal with them separately - if arg.find(' ') > 0: - filo = arg.split() - filo.reverse() - is_flag = lambda x: x.startswith('-') - new_arg, new_value = str(), str() - while len(filo) > 0: - if is_flag(filo[0]): - new_arg = filo.pop() - if len(filo) > 0: - while not is_flag(filo[0]): - new_value += (filo.pop() + ' ') - else: - logger.debug("Got non-flag argument: %s" % filo[0]) - filo.pop() - safe = _check_arg_and_value(new_arg, new_value) - if safe is not None and not safe.strip() == '': - logger.debug("_sanitise(): appending args: %s" % safe) - checked.append(safe) - else: - safe = _check_arg_and_value(arg, None) - logger.debug("_sanitise(): appending args: %s" % safe) - checked.append(safe) - elif isinstance(arg, list): ## happens with '--version' - logger.debug("_sanitise(): Got arg list: %s" % arg) - for a in arg: - if a.startswith('--'): - safe = _check_arg_and_value(a, None) - logger.debug("_sanitise(): appending args: %s" % safe) - checked.append(safe) - else: - logger.debug("_sanitise(): got non string or list arg: %s" % arg) - - sanitised = ' '.join(x for x in checked) - return sanitised - -def _sanitise_list(arg_list): - """ - A generator for running through a list of gpg options and sanitising them. - - :type arg_list: C{list} - :param arg_list: A list of options and flags for gpg. - :rtype: C{generator} - :return: A generator whose next() method returns each of the items in - :param:arg_list after calling :func:_sanitise with that item as a - parameter. - """ - if isinstance(arg_list, list): - for arg in arg_list: - safe_arg = _sanitise(arg) - if safe_arg != "": - yield safe_arg - def _threaded_copy_data(instream, outstream): wr = threading.Thread(target=_copy_data, args=(instream, outstream)) wr.setDaemon(True) @@ -695,362 +278,7 @@ def _write_passphrase(stream, passphrase, encoding): stream.write(passphrase) logger.debug("Wrote passphrase.") - -class Verify(object): - """Handle status messages for --verify""" - - TRUST_UNDEFINED = 0 - TRUST_NEVER = 1 - TRUST_MARGINAL = 2 - TRUST_FULLY = 3 - TRUST_ULTIMATE = 4 - - TRUST_LEVELS = { - "TRUST_UNDEFINED" : TRUST_UNDEFINED, - "TRUST_NEVER" : TRUST_NEVER, - "TRUST_MARGINAL" : TRUST_MARGINAL, - "TRUST_FULLY" : TRUST_FULLY, - "TRUST_ULTIMATE" : TRUST_ULTIMATE, - } - - def __init__(self, gpg): - self.gpg = gpg - self.valid = False - self.fingerprint = self.creation_date = self.timestamp = None - self.signature_id = self.key_id = None - self.username = None - self.status = None - self.pubkey_fingerprint = None - self.expire_timestamp = None - self.sig_timestamp = None - self.trust_text = None - self.trust_level = None - - def __nonzero__(self): - return self.valid - - __bool__ = __nonzero__ - - def handle_status(self, key, value): - if key in self.TRUST_LEVELS: - self.trust_text = key - self.trust_level = self.TRUST_LEVELS[key] - elif key in ("RSA_OR_IDEA", "NODATA", "IMPORT_RES", "PLAINTEXT", - "PLAINTEXT_LENGTH", "POLICY_URL", "DECRYPTION_INFO", - "DECRYPTION_OKAY", "INV_SGNR"): - pass - elif key == "BADSIG": - self.valid = False - self.status = 'signature bad' - self.key_id, self.username = value.split(None, 1) - elif key == "GOODSIG": - self.valid = True - self.status = 'signature good' - self.key_id, self.username = value.split(None, 1) - elif key == "VALIDSIG": - (self.fingerprint, - self.creation_date, - self.sig_timestamp, - self.expire_timestamp) = value.split()[:4] - # may be different if signature is made with a subkey - self.pubkey_fingerprint = value.split()[-1] - self.status = 'signature valid' - elif key == "SIG_ID": - (self.signature_id, - self.creation_date, self.timestamp) = value.split() - elif key == "ERRSIG": - self.valid = False - (self.key_id, - algo, hash_algo, - cls, - self.timestamp) = value.split()[:5] - self.status = 'signature error' - elif key == "DECRYPTION_FAILED": - self.valid = False - self.key_id = value - self.status = 'decryption failed' - elif key == "NO_PUBKEY": - self.valid = False - self.key_id = value - self.status = 'no public key' - elif key in ("KEYEXPIRED", "SIGEXPIRED"): - # these are useless in verify, since they are spit out for any - # pub/subkeys on the key, not just the one doing the signing. - # if we want to check for signatures with expired key, - # the relevant flag is EXPKEYSIG. - pass - elif key in ("EXPKEYSIG", "REVKEYSIG"): - # signed with expired or revoked key - self.valid = False - self.key_id = value.split()[0] - self.status = (('%s %s') % (key[:3], key[3:])).lower() - else: - raise ValueError("Unknown status message: %r" % key) - -class ImportResult(object): - """Handle status messages for --import""" - - counts = '''count no_user_id imported imported_rsa unchanged - n_uids n_subk n_sigs n_revoc sec_read sec_imported - sec_dups not_imported'''.split() - def __init__(self, gpg): - self.gpg = gpg - self.imported = [] - self.results = [] - self.fingerprints = [] - for result in self.counts: - setattr(self, result, None) - - def __nonzero__(self): - if self.not_imported: return False - if not self.fingerprints: return False - return True - - __bool__ = __nonzero__ - - ok_reason = { - '0': 'Not actually changed', - '1': 'Entirely new key', - '2': 'New user IDs', - '4': 'New signatures', - '8': 'New subkeys', - '16': 'Contains private key', - } - - problem_reason = { - '0': 'No specific reason given', - '1': 'Invalid Certificate', - '2': 'Issuer Certificate missing', - '3': 'Certificate Chain too long', - '4': 'Error storing certificate', - } - - def handle_status(self, key, value): - if key == "IMPORTED": - # this duplicates info we already see in import_ok & import_problem - pass - elif key == "NODATA": - self.results.append({'fingerprint': None, - 'problem': '0', 'text': 'No valid data found'}) - elif key == "IMPORT_OK": - reason, fingerprint = value.split() - reasons = [] - for code, text in list(self.ok_reason.items()): - if int(reason) | int(code) == int(reason): - reasons.append(text) - reasontext = '\n'.join(reasons) + "\n" - self.results.append({'fingerprint': fingerprint, - 'ok': reason, 'text': reasontext}) - self.fingerprints.append(fingerprint) - elif key == "IMPORT_PROBLEM": - try: - reason, fingerprint = value.split() - except: - reason = value - fingerprint = '' - self.results.append({'fingerprint': fingerprint, - 'problem': reason, 'text': self.problem_reason[reason]}) - elif key == "IMPORT_RES": - import_res = value.split() - for i in range(len(self.counts)): - setattr(self, self.counts[i], int(import_res[i])) - elif key == "KEYEXPIRED": - self.results.append({'fingerprint': None, - 'problem': '0', 'text': 'Key expired'}) - elif key == "SIGEXPIRED": - self.results.append({'fingerprint': None, - 'problem': '0', 'text': 'Signature expired'}) - else: - raise ValueError("Unknown status message: %r" % key) - - def summary(self): - l = [] - l.append('%d imported' % self.imported) - if self.not_imported: - l.append('%d not imported' % self.not_imported) - return ', '.join(l) - -class ListKeys(list): - """ Handle status messages for --list-keys. - - Handle pub and uid (relating the latter to the former). - - Don't care about (info from src/DETAILS): - - crt = X.509 certificate - crs = X.509 certificate and private key available - ssb = secret subkey (secondary key) - uat = user attribute (same as user id except for field 10). - sig = signature - rev = revocation signature - pkd = public key data (special field format, see below) - grp = reserved for gpgsm - rvk = revocation key - """ - - def __init__(self, gpg): - super(ListKeys, self).__init__() - self.gpg = gpg - self.curkey = None - self.fingerprints = [] - self.uids = [] - - def key(self, args): - vars = (""" - type trust length algo keyid date expires dummy ownertrust uid - """).split() - self.curkey = {} - for i in range(len(vars)): - self.curkey[vars[i]] = args[i] - self.curkey['uids'] = [] - if self.curkey['uid']: - self.curkey['uids'].append(self.curkey['uid']) - del self.curkey['uid'] - self.curkey['subkeys'] = [] - self.append(self.curkey) - - pub = sec = key - - def fpr(self, args): - self.curkey['fingerprint'] = args[9] - self.fingerprints.append(args[9]) - - def uid(self, args): - uid = args[9] - uid = ESCAPE_PATTERN.sub(lambda m: chr(int(m.group(1), 16)), uid) - self.curkey['uids'].append(uid) - self.uids.append(uid) - - def sub(self, args): - subkey = [args[4], args[11]] - self.curkey['subkeys'].append(subkey) - - def handle_status(self, key, value): - pass - -class Crypt(Verify): - """Handle status messages for --encrypt and --decrypt""" - def __init__(self, gpg): - super(Crypt, self).__init__(self, gpg) - self.data = '' - self.ok = False - self.status = '' - - def __nonzero__(self): - if self.ok: return True - return False - - __bool__ = __nonzero__ - - def __str__(self): - return self.data.decode(self.gpg.encoding, self.gpg.decode_errors) - - def handle_status(self, key, value): - if key in ("ENC_TO", "USERID_HINT", "GOODMDC", "END_DECRYPTION", - "BEGIN_SIGNING", "NO_SECKEY", "ERROR", "NODATA", - "CARDCTRL"): - # in the case of ERROR, this is because a more specific error - # message will have come first - pass - elif key in ("NEED_PASSPHRASE", "BAD_PASSPHRASE", "GOOD_PASSPHRASE", - "MISSING_PASSPHRASE", "DECRYPTION_FAILED", - "KEY_NOT_CREATED"): - self.status = key.replace("_", " ").lower() - elif key == "NEED_PASSPHRASE_SYM": - self.status = 'need symmetric passphrase' - elif key == "BEGIN_DECRYPTION": - self.status = 'decryption incomplete' - elif key == "BEGIN_ENCRYPTION": - self.status = 'encryption incomplete' - elif key == "DECRYPTION_OKAY": - self.status = 'decryption ok' - self.ok = True - elif key == "END_ENCRYPTION": - self.status = 'encryption ok' - self.ok = True - elif key == "INV_RECP": - self.status = 'invalid recipient' - elif key == "KEYEXPIRED": - self.status = 'key expired' - elif key == "SIG_CREATED": - self.status = 'sig created' - elif key == "SIGEXPIRED": - self.status = 'sig expired' - else: - Verify.handle_status(self, key, value) - -class GenKey(object): - """Handle status messages for --gen-key""" - def __init__(self, gpg): - self.gpg = gpg - self.type = None - self.fingerprint = None - - def __nonzero__(self): - if self.fingerprint: return True - return False - - __bool__ = __nonzero__ - - def __str__(self): - return self.fingerprint or '' - - def handle_status(self, key, value): - if key in ("PROGRESS", "GOOD_PASSPHRASE", "NODATA", "KEY_NOT_CREATED"): - pass - elif key == "KEY_CREATED": - (self.type, self.fingerprint) = value.split() - else: - raise ValueError("Unknown status message: %r" % key) - -class DeleteResult(object): - """Handle status messages for --delete-key and --delete-secret-key""" - def __init__(self, gpg): - self.gpg = gpg - self.status = 'ok' - - def __str__(self): - return self.status - - problem_reason = { - '1': 'No such key', - '2': 'Must delete secret key first', - '3': 'Ambigious specification', - } - - def handle_status(self, key, value): - if key == "DELETE_PROBLEM": - self.status = self.problem_reason.get(value, "Unknown error: %r" - % value) - else: - raise ValueError("Unknown status message: %r" % key) - -class Sign(object): - """Handle status messages for --sign""" - def __init__(self, gpg): - self.gpg = gpg - self.type = None - self.fingerprint = None - - def __nonzero__(self): - return self.fingerprint is not None - - __bool__ = __nonzero__ - - def __str__(self): - return self.data.decode(self.gpg.encoding, self.gpg.decode_errors) - - def handle_status(self, key, value): - if key in ("USERID_HINT", "NEED_PASSPHRASE", "BAD_PASSPHRASE", - "GOOD_PASSPHRASE", "BEGIN_SIGNING", "CARDCTRL", - "INV_SGNR", "NODATA"): - pass - elif key == "SIG_CREATED": - (self.type, algo, hashalgo, cls, self.timestamp, - self.fingerprint) = value.split() - else: - raise ValueError("Unknown status message: %r" % key) - + class GPG(object): """Encapsulate access to the gpg executable""" decode_errors = 'strict' diff --git a/gnupg/parsers.py b/gnupg/parsers.py new file mode 100644 index 0000000..4a2b29b --- /dev/null +++ b/gnupg/parsers.py @@ -0,0 +1,833 @@ +#!/usr/bin/env python +#-*- encoding: utf-8 -*- +# +# This file is part of python-gnupg, a Python wrapper around GnuPG. +# Copyright Ā© 2013 Isis Lovecruft +# Ā© 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 Affero 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 +# GNU Affero General Public License for more details. +''' +parsers.py +---------- +Classes for parsing GnuPG status messages and sanitising commandline options. +''' + + +class ProtectedOption(Exception): + """Raised when the option passed to GPG is disallowed.""" + +class UsageError(Exception): + """Raised when incorrect usage of the API occurs..""" + + +def _fix_unsafe(input): + """ + Find characters used to escape from a string into a shell, and wrap them + in quotes if they exist. Regex pilfered from python-3.x shlex module. + + :param input: The input intended for the gnupg process. + """ + ## xxx do we want to add ';'? + _unsafe = re.compile(r'[^\w@%+=:,./-]', 256) + try: + if len(_unsafe.findall(input)) == 0: + return input + else: + clean = "'" + input.replace("'", "'\"'\"'") + "'" + return clean + except TypeError: + return None + +def _hyphenate(input, add_prefix=False): + """ + Change underscores to hyphens so that object attributes can be easily + tranlated to GPG option names. + + :type input: C{str} + :param input: The attribute to hyphenate. + + :type add_prefix: C{bool} + :param add_prefix: If True, add leading hyphens to the input. + + :rtype: C{str} + :return: The :param:input with underscores changed to hyphens. + """ + ret = '--' if add_prefix else '' + ret += input.replace('_', '-') + return ret + +def _is_allowed(input): + """ + Check that an option or argument given to GPG is in the set of allowed + options, the latter being a strict subset of the set of all options known + to GPG. + + :type input: C{str} + :param input: An input meant to be parsed as an option or flag to the GnuPG + process. Should be formatted the same as an option or flag + to the commandline gpg, i.e. "--encrypt-files". + + :type _possible: C{frozenset} + :ivar _possible: All known GPG options and flags. + + :type _allowed: C{frozenset} + :ivar _allowed: All allowed GPG options and flags, e.g. all GPG options and + flags which we are willing to acknowledge and parse. If we + want to support a new option, it will need to have its own + parsing class and its name will need to be added to this + set. + + :rtype: C{Exception} or C{str} + :raise: UsageError if :ivar:_allowed is not a subset of :ivar:_possible. + ProtectedOption if :param:input is not in the set :ivar:_allowed. + :return: The original parameter :param:input, unmodified and unsanitized, + if no errors occur. + """ + + _all = (""" +--allow-freeform-uid --multifile +--allow-multiple-messages --no +--allow-multisig-verification --no-allow-freeform-uid +--allow-non-selfsigned-uid --no-allow-multiple-messages +--allow-secret-key-import --no-allow-non-selfsigned-uid +--always-trust --no-armor +--armor --no-armour +--armour --no-ask-cert-expire +--ask-cert-expire --no-ask-cert-level +--ask-cert-level --no-ask-sig-expire +--ask-sig-expire --no-auto-check-trustdb +--attribute-fd --no-auto-key-locate +--attribute-file --no-auto-key-retrieve +--auto-check-trustdb --no-batch +--auto-key-locate --no-comments +--auto-key-retrieve --no-default-keyring +--batch --no-default-recipient +--bzip2-compress-level --no-disable-mdc +--bzip2-decompress-lowmem --no-emit-version +--card-edit --no-encrypt-to +--card-status --no-escape-from-lines +--cert-digest-algo --no-expensive-trust-checks +--cert-notation --no-expert +--cert-policy-url --no-force-mdc +--change-pin --no-force-v3-sigs +--charset --no-force-v4-certs +--check-sig --no-for-your-eyes-only +--check-sigs --no-greeting +--check-trustdb --no-groups +--cipher-algo --no-literal +--clearsign --no-mangle-dos-filenames +--command-fd --no-mdc-warning +--command-file --no-options +--comment --no-permission-warning +--completes-needed --no-pgp2 +--compress-algo --no-pgp6 +--compression-algo --no-pgp7 +--compress-keys --no-pgp8 +--compress-level --no-random-seed-file +--compress-sigs --no-require-backsigs +--ctapi-driver --no-require-cross-certification +--dearmor --no-require-secmem +--dearmour --no-rfc2440-text +--debug --no-secmem-warning +--debug-all --no-show-notation +--debug-ccid-driver --no-show-photos +--debug-level --no-show-policy-url +--decrypt --no-sig-cache +--decrypt-files --no-sig-create-check +--default-cert-check-level --no-sk-comments +--default-cert-expire --no-strict +--default-cert-level --notation-data +--default-comment --not-dash-escaped +--default-key --no-textmode +--default-keyserver-url --no-throw-keyid +--default-preference-list --no-throw-keyids +--default-recipient --no-tty +--default-recipient-self --no-use-agent +--default-sig-expire --no-use-embedded-filename +--delete-keys --no-utf8-strings +--delete-secret-and-public-keys --no-verbose +--delete-secret-keys --no-version +--desig-revoke --openpgp +--detach-sign --options +--digest-algo --output +--disable-ccid --override-session-key +--disable-cipher-algo --passphrase +--disable-dsa2 --passphrase-fd +--disable-mdc --passphrase-file +--disable-pubkey-algo --passphrase-repeat +--display --pcsc-driver +--display-charset --personal-cipher-preferences +--dry-run --personal-cipher-prefs +--dump-options --personal-compress-preferences +--edit-key --personal-compress-prefs +--emit-version --personal-digest-preferences +--enable-dsa2 --personal-digest-prefs +--enable-progress-filter --pgp2 +--enable-special-filenames --pgp6 +--enarmor --pgp7 +--enarmour --pgp8 +--encrypt --photo-viewer +--encrypt-files --pipemode +--encrypt-to --preserve-permissions +--escape-from-lines --primary-keyring +--exec-path --print-md +--exit-on-status-write-error --print-mds +--expert --quick-random +--export --quiet +--export-options --reader-port +--export-ownertrust --rebuild-keydb-caches +--export-secret-keys --recipient +--export-secret-subkeys --recv-keys +--fast-import --refresh-keys +--fast-list-mode --remote-user +--fetch-keys --require-backsigs +--fingerprint --require-cross-certification +--fixed-list-mode --require-secmem +--fix-trustdb --rfc1991 +--force-mdc --rfc2440 +--force-ownertrust --rfc2440-text +--force-v3-sigs --rfc4880 +--force-v4-certs --run-as-shm-coprocess +--for-your-eyes-only --s2k-cipher-algo +--gen-key --s2k-count +--gen-prime --s2k-digest-algo +--gen-random --s2k-mode +--gen-revoke --search-keys +--gnupg --secret-keyring +--gpg-agent-info --send-keys +--gpgconf-list --set-filename +--gpgconf-test --set-filesize +--group --set-notation +--help --set-policy-url +--hidden-encrypt-to --show-keyring +--hidden-recipient --show-notation +--homedir --show-photos +--honor-http-proxy --show-policy-url +--ignore-crc-error --show-session-key +--ignore-mdc-error --sig-keyserver-url +--ignore-time-conflict --sign +--ignore-valid-from --sign-key +--import --sig-notation +--import-options --sign-with +--import-ownertrust --sig-policy-url +--interactive --simple-sk-checksum +--keyid-format --sk-comments +--keyring --skip-verify +--keyserver --status-fd +--keyserver-options --status-file +--lc-ctype --store +--lc-messages --strict +--limit-card-insert-tries --symmetric +--list-config --temp-directory +--list-key --textmode +--list-keys --throw-keyid +--list-only --throw-keyids +--list-options --trustdb-name +--list-ownertrust --trusted-key +--list-packets --trust-model +--list-public-keys --try-all-secrets +--list-secret-keys --ttyname +--list-sig --ttytype +--list-sigs --ungroup +--list-trustdb --update-trustdb +--load-extension --use-agent +--local-user --use-embedded-filename +--lock-multiple --user +--lock-never --utf8-strings +--lock-once --verbose +--logger-fd --verify +--logger-file --verify-files +--lsign-key --verify-options +--mangle-dos-filenames --version +--marginals-needed --warranty +--max-cert-depth --with-colons +--max-output --with-fingerprint +--merge-only --with-key-data +--min-cert-level --yes +""").split() + + _possible = frozenset(_all) + + ## these are the allowed options we will handle so far, all others should + ## be dropped. this dance is so that when new options are added later, we + ## merely add the to the _allowed list, and the `` _allowed.issubset`` + ## assertion will check that GPG will recognise them + ## + ## xxx checkout the --store option for creating rfc1991 data packets + ## xxx also --multifile use with verify encrypt & decrypt + ## xxx key fetching/retrieving options: [fetch_keys, merge_only, recv_keys] + ## + ## xxx which ones do we want as defaults? + ## eg, --no-show-photos would mitigate things like + ## https://www-01.ibm.com/support/docview.wss?uid=swg21620982 + _allowed = frozenset( + ['--list-keys', '--list-key', '--fixed-list-mode', + '--list-secret-keys', '--list-public-keys', + '--list-packets', '--with-colons', + '--delete-keys', '--delete-secret-keys', + '--encrypt', '--encrypt-files', + '--decrypt', '--decrypt-files', + '--print-mds', '--print-md', + '--sign', '--clearsign', '--detach-sign', + '--gen-key', '--batch', + '--decrypt', '--decrypt-files', + '--import', + '--export', '--export-secret-keys', '--export-secret-subkeys', + '--verify', + '--version', '--output', + '--status-fd', '--no-tty', '--passphrase-fd', + '--homedir', '--no-default-keyring', + '--keyring', '--secret-keyring', '--primary-keyring', + '--fingerprint',]) + + ## check that _allowed is a subset of _possible + try: + assert _allowed.issubset(_possible), \ + '_allowed is not subset of known options, difference: %s' \ + % _allowed.difference(_possible) + except AssertionError as ae: + logger.debug("gnupg._is_allowed(): %s" % ae.message) + raise UsageError(ae.message) + + ## if we got a list of args, join them + if not isinstance(input, str): + input = ' '.join([x for x in input]) + + if isinstance(input, str): + if input.find('_') > 0: + if not input.startswith('--'): + hyphenated = _hyphenate(input, add_prefix=True) + else: + hyphenated = _hyphenate(input) + else: + hyphenated = input + try: + assert hyphenated in _allowed + except AssertionError as ae: + logger.warn("_is_allowed(): Dropping option '%s'..." + % _fix_unsafe(hyphenated)) + raise ProtectedOption("Option '%s' not supported." + % _fix_unsafe(hyphenated)) + else: + return input + return None + +def _sanitise(*args): + """ + Take an arg or the key portion of a kwarg and check that it is in the set + of allowed GPG options and flags, and that it has the correct type. Then, + attempt to escape any unsafe characters. If an option is not allowed, + drop it with a logged warning. Returns a dictionary of all sanitised, + allowed options. + + 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 + safety checks added here. + + GnuPG has three-hundred and eighteen commandline flags. Also, not all + implementations of OpenPGP parse PGP packets and headers in the same way, + so there is added potential there for messing with calls to GPG. + + For information on the PGP message format specification, see: + https://www.ietf.org/rfc/rfc1991.txt + + If you're asking, "Is this *really* necessary?": No. Not really. See: + https://xkcd.com/1181/ + + :type args: C{str} + :param args: (optional) The boolean arguments which will be passed to the + GnuPG process. + :rtype: C{str} + :param: :ivar:sanitised + """ + + def _check_arg_and_value(arg, value): + """ + 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 + :ivar:sanitised. + + :type arg: C{str} + + :param arg: The arguments which will be passed to the GnuPG process, + and, optionally their corresponding values. The values are + any additional arguments following the GnuPG option or + flag. For example, if we wanted to pass "--encrypt + --recipient isis@leap.se" to gpg, then "--encrypt" would be + an arg without a value, and "--recipient" would also be an + arg, with a value of "isis@leap.se". + :type checked: C{list} + :ivar checked: The sanitised, allowed options and values. + :rtype: C{str} + :returns: A string of the items in :ivar:`checked` delimited by spaces. + """ + safe_values = str() + + try: + allowed_flag = _is_allowed(arg) + assert allowed_flag is not None, \ + "_check_arg_and_value(): got None for allowed_flag" + except (AssertionError, ProtectedOption) as error: + logger.warn("_sanitise(): %s" % error.message) + else: + safe_values += (allowed_flag + " ") + if isinstance(value, str): + value_list = value.split(' ') + for value in value_list: + safe_value = _fix_unsafe(value) + if safe_value is not None and not safe_value.strip() == "": + if allowed_flag in ['--encrypt', '--encrypt-files', + '--decrypt', '--decrypt-file', + '--import', '--verify']: + ## Place checks here: + if _is_file(safe_value): + safe_values += (safe_value + " ") + else: + logger.debug( + "_sanitize(): Option %s not file: %s" + % (allowed_flag, safe_value)) + else: + safe_values += (safe_value + " ") + logger.debug( + "_sanitize(): No configured checks for: %s" + % safe_value) + return safe_values + + checked = [] + + if args is not None: + for arg in args: + if isinstance(arg, str): + logger.debug("_sanitise(): Got arg string: %s" % arg) + ## if we're given a string with a bunch of options in it split + ## them up and deal with them separately + if arg.find(' ') > 0: + filo = arg.split() + filo.reverse() + is_flag = lambda x: x.startswith('-') + new_arg, new_value = str(), str() + while len(filo) > 0: + if is_flag(filo[0]): + new_arg = filo.pop() + if len(filo) > 0: + while not is_flag(filo[0]): + new_value += (filo.pop() + ' ') + else: + logger.debug("Got non-flag argument: %s" % filo[0]) + filo.pop() + safe = _check_arg_and_value(new_arg, new_value) + if safe is not None and not safe.strip() == '': + logger.debug("_sanitise(): appending args: %s" + % safe) + checked.append(safe) + else: + safe = _check_arg_and_value(arg, None) + logger.debug("_sanitise(): appending args: %s" % safe) + checked.append(safe) + elif isinstance(arg, list): ## happens with '--version' + logger.debug("_sanitise(): Got arg list: %s" % arg) + for a in arg: + if a.startswith('--'): + safe = _check_arg_and_value(a, None) + logger.debug("_sanitise(): appending args: %s" % safe) + checked.append(safe) + else: + logger.debug("_sanitise(): got non string or list arg: %s" + % arg) + + sanitised = ' '.join(x for x in checked) + return sanitised + +def _sanitise_list(arg_list): + """ + A generator for running through a list of gpg options and sanitising them. + + :type arg_list: C{list} + :param arg_list: A list of options and flags for gpg. + :rtype: C{generator} + :return: A generator whose next() method returns each of the items in + :param:arg_list after calling :func:_sanitise with that item as a + parameter. + """ + if isinstance(arg_list, list): + for arg in arg_list: + safe_arg = _sanitise(arg) + if safe_arg != "": + yield safe_arg + + +class Crypt(Verify): + """Handle status messages for --encrypt and --decrypt""" + def __init__(self, gpg): + super(Crypt, self).__init__(self, gpg) + self.data = '' + self.ok = False + self.status = '' + + def __nonzero__(self): + if self.ok: return True + return False + + __bool__ = __nonzero__ + + def __str__(self): + return self.data.decode(self.gpg.encoding, self.gpg.decode_errors) + + def handle_status(self, key, value): + """Parse a status code from the attached GnuPG process. + + :raises: :class:`ValueError` if the status message is unknown. + """ + if key in ("ENC_TO", "USERID_HINT", "GOODMDC", "END_DECRYPTION", + "BEGIN_SIGNING", "NO_SECKEY", "ERROR", "NODATA", + "CARDCTRL"): + # in the case of ERROR, this is because a more specific error + # message will have come first + pass + elif key in ("NEED_PASSPHRASE", "BAD_PASSPHRASE", "GOOD_PASSPHRASE", + "MISSING_PASSPHRASE", "DECRYPTION_FAILED", + "KEY_NOT_CREATED"): + self.status = key.replace("_", " ").lower() + elif key == "NEED_PASSPHRASE_SYM": + self.status = 'need symmetric passphrase' + elif key == "BEGIN_DECRYPTION": + self.status = 'decryption incomplete' + elif key == "BEGIN_ENCRYPTION": + self.status = 'encryption incomplete' + elif key == "DECRYPTION_OKAY": + self.status = 'decryption ok' + self.ok = True + elif key == "END_ENCRYPTION": + self.status = 'encryption ok' + self.ok = True + elif key == "INV_RECP": + self.status = 'invalid recipient' + elif key == "KEYEXPIRED": + self.status = 'key expired' + elif key == "SIG_CREATED": + self.status = 'sig created' + elif key == "SIGEXPIRED": + self.status = 'sig expired' + else: + Verify.handle_status(self, key, value) + +class GenKey(object): + """Handle status messages for --gen-key""" + def __init__(self, gpg): + self.gpg = gpg + self.type = None + self.fingerprint = None + + def __nonzero__(self): + if self.fingerprint: return True + return False + + __bool__ = __nonzero__ + + def __str__(self): + return self.fingerprint or '' + + def handle_status(self, key, value): + """Parse a status code from the attached GnuPG process. + + :raises: :class:`ValueError` if the status message is unknown. + """ + if key in ("PROGRESS", "GOOD_PASSPHRASE", "NODATA", "KEY_NOT_CREATED"): + pass + elif key == "KEY_CREATED": + (self.type, self.fingerprint) = value.split() + else: + raise ValueError("Unknown status message: %r" % key) + +class DeleteResult(object): + """Handle status messages for --delete-key and --delete-secret-key""" + def __init__(self, gpg): + self.gpg = gpg + self.status = 'ok' + + def __str__(self): + return self.status + + problem_reason = { + '1': 'No such key', + '2': 'Must delete secret key first', + '3': 'Ambigious specification', + } + + def handle_status(self, key, value): + """Parse a status code from the attached GnuPG process. + + :raises: :class:`ValueError` if the status message is unknown. + """ + if key == "DELETE_PROBLEM": + self.status = self.problem_reason.get(value, "Unknown error: %r" + % value) + else: + raise ValueError("Unknown status message: %r" % key) + +class Sign(object): + """Parse GnuPG status messages for signing operations. + + :param gpg: An instance of :class:`gnupg.GPG`. + :type sig_type: :type:`str` + :attr sig_type: The type of signature created. + :type fingerprint: :type:`str` + :attr fingerprint: The fingerprint of the signing keyID. + """ + + def __init__(self, gpg): + self.gpg = gpg + self.sig_type = None + self.fingerprint = None + + def __nonzero__(self): + """Override the determination for truthfulness evaluation. + + :rtype: :type:`bool` + :returns: True if we have a valid signature, False otherwise. + """ + return self.fingerprint is not None + __bool__ = __nonzero__ + + def __str__(self): + return self.data.decode(self.gpg.encoding, self.gpg.decode_errors) + + def handle_status(self, key, value): + """Parse a status code from the attached GnuPG process. + + :raises: :class:`ValueError` if the status message is unknown. + """ + if key in ("USERID_HINT", "NEED_PASSPHRASE", "BAD_PASSPHRASE", + "GOOD_PASSPHRASE", "BEGIN_SIGNING", "CARDCTRL", + "INV_SGNR", "NODATA"): + pass + elif key == "SIG_CREATED": + (self.sig_type, algo, hashalgo, cls, self.timestamp, + self.fingerprint) = value.split() + else: + raise ValueError("Unknown status message: %r" % key) + +class ImportResult(object): + """Parse GnuPG status messages for key import operations. + + :type gpg: :class:`gnupg.GPG` + :param gpg: An instance of :class:`gnupg.GPG`. + :type imported: :type:`list` + :attr imported: List of all keys imported. + :type fingerprints: :type:`list` + :attr fingerprints: A list of strings of the GnuPG keyIDs imported. + :type results: :type:`list` + :attr results: A list containing dictionaries with information gathered + on keys imported. + """ + + counts = '''count no_user_id imported imported_rsa unchanged + n_uids n_subk n_sigs n_revoc sec_read sec_imported + sec_dups not_imported'''.split() + def __init__(self, gpg): + self.gpg = gpg + self.imported = [] + self.results = [] + self.fingerprints = [] + for result in self.counts: + setattr(self, result, None) + + def __nonzero__(self): + """Override the determination for truthfulness evaluation. + + :rtype: :type:`bool` + :returns: True if we have immport some keys, False otherwise. + """ + if self.not_imported: return False + if not self.fingerprints: return False + return True + __bool__ = __nonzero__ + + ok_reason = {'0': 'Not actually changed', + '1': 'Entirely new key', + '2': 'New user IDs', + '4': 'New signatures', + '8': 'New subkeys', + '16': 'Contains private key',} + + problem_reason = { '0': 'No specific reason given', + '1': 'Invalid Certificate', + '2': 'Issuer Certificate missing', + '3': 'Certificate Chain too long', + '4': 'Error storing certificate', } + + def handle_status(self, key, value): + """Parse a status code from the attached GnuPG process. + + :raises: :class:`ValueError` if the status message is unknown. + """ + if key == "IMPORTED": + # this duplicates info we already see in import_ok & import_problem + pass + elif key == "NODATA": + self.results.append({'fingerprint': None, + 'problem': '0', 'text': 'No valid data found'}) + elif key == "IMPORT_OK": + reason, fingerprint = value.split() + reasons = [] + for code, text in list(self.ok_reason.items()): + if int(reason) | int(code) == int(reason): + reasons.append(text) + reasontext = '\n'.join(reasons) + "\n" + self.results.append({'fingerprint': fingerprint, + 'ok': reason, 'text': reasontext}) + self.fingerprints.append(fingerprint) + elif key == "IMPORT_PROBLEM": + try: + reason, fingerprint = value.split() + except: + reason = value + fingerprint = '' + self.results.append({'fingerprint': fingerprint, + 'problem': reason, 'text': self.problem_reason[reason]}) + elif key == "IMPORT_RES": + import_res = value.split() + for i in range(len(self.counts)): + setattr(self, self.counts[i], int(import_res[i])) + elif key == "KEYEXPIRED": + self.results.append({'fingerprint': None, + 'problem': '0', 'text': 'Key expired'}) + elif key == "SIGEXPIRED": + self.results.append({'fingerprint': None, + 'problem': '0', 'text': 'Signature expired'}) + else: + raise ValueError("Unknown status message: %r" % key) + + def summary(self): + l = [] + l.append('%d imported' % self.imported) + if self.not_imported: + l.append('%d not imported' % self.not_imported) + return ', '.join(l) + + +class Verify(object): + """Classes for parsing GnuPG status messages for signature verification. + + :type gpg: :class:`gnupg.GPG` + :param gpg: An instance of :class:`gnupg.GPG`. + :type valid: :type:`bool` + :attr valid: True if the signature or file was verified successfully, + False otherwise. + :type fingerprint: :type:`str` + :attr fingerprint: The fingerprint of the GnuPG keyID which created the + signature. + :type creation_date: :type:`str` + :attr creation_date: The date the signature was made. + :type timestamp: :type:`str` + :attr timestamp: The timestamp used internally in the signature. + :type signature_id: :type:`str` + :attr signature_id: The uid of the signing GnuPG key. + :type status: :type:`str` + :attr status: The internal status message from the GnuPG process. + """ + ## xxx finish documentation + + TRUST_UNDEFINED = 0 + TRUST_NEVER = 1 + TRUST_MARGINAL = 2 + TRUST_FULLY = 3 + TRUST_ULTIMATE = 4 + + TRUST_LEVELS = {"TRUST_UNDEFINED" : TRUST_UNDEFINED, + "TRUST_NEVER" : TRUST_NEVER, + "TRUST_MARGINAL" : TRUST_MARGINAL, + "TRUST_FULLY" : TRUST_FULLY, + "TRUST_ULTIMATE" : TRUST_ULTIMATE,} + + def __init__(self, gpg): + self.gpg = gpg + self.valid = False + self.fingerprint = self.creation_date = self.timestamp = None + self.signature_id = self.key_id = None + self.username = None + self.status = None + self.pubkey_fingerprint = None + self.expire_timestamp = None + self.sig_timestamp = None + self.trust_text = None + self.trust_level = None + + def __nonzero__(self): + """Override the determination for truthfulness evaluation. + + :rtype: :type bool: + :returns: True if we have a valid signature, False otherwise. + """ + return self.valid + __bool__ = __nonzero__ + + def handle_status(self, key, value): + """Parse a status code from the attached GnuPG process. + + :raises: :class:`ValueError` if the status message is unknown. + """ + if key in self.TRUST_LEVELS: + self.trust_text = key + self.trust_level = self.TRUST_LEVELS[key] + elif key in ("RSA_OR_IDEA", "NODATA", "IMPORT_RES", "PLAINTEXT", + "PLAINTEXT_LENGTH", "POLICY_URL", "DECRYPTION_INFO", + "DECRYPTION_OKAY", "INV_SGNR"): + pass + elif key == "BADSIG": + self.valid = False + self.status = 'signature bad' + self.key_id, self.username = value.split(None, 1) + elif key == "GOODSIG": + self.valid = True + self.status = 'signature good' + self.key_id, self.username = value.split(None, 1) + elif key == "VALIDSIG": + (self.fingerprint, + self.creation_date, + self.sig_timestamp, + self.expire_timestamp) = value.split()[:4] + # may be different if signature is made with a subkey + self.pubkey_fingerprint = value.split()[-1] + self.status = 'signature valid' + elif key == "SIG_ID": + (self.signature_id, + self.creation_date, self.timestamp) = value.split() + elif key == "ERRSIG": + self.valid = False + (self.key_id, + algo, hash_algo, + cls, + self.timestamp) = value.split()[:5] + self.status = 'signature error' + elif key == "DECRYPTION_FAILED": + self.valid = False + self.key_id = value + self.status = 'decryption failed' + elif key == "NO_PUBKEY": + self.valid = False + self.key_id = value + self.status = 'no public key' + elif key in ("KEYEXPIRED", "SIGEXPIRED"): + # these are useless in verify, since they are spit out for any + # pub/subkeys on the key, not just the one doing the signing. + # if we want to check for signatures with expired key, + # the relevant flag is EXPKEYSIG. + pass + elif key in ("EXPKEYSIG", "REVKEYSIG"): + # signed with expired or revoked key + self.valid = False + self.key_id = value.split()[0] + self.status = (('%s %s') % (key[:3], key[3:])).lower() + else: + raise ValueError("Unknown status message: %r" % key) diff --git a/test_gnupg.py b/gnupg/tests/test_gnupg.py similarity index 100% rename from test_gnupg.py rename to gnupg/tests/test_gnupg.py diff --git a/util.py b/gnupg/util.py similarity index 100% rename from util.py rename to gnupg/util.py From 6f9e0b4a301f5da5c6892b489e5bbcc564a5a408 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 11 Apr 2013 23:46:49 +0000 Subject: [PATCH 067/397] Update top-level Makefile with new directory structure. --- Makefile | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 5792a18..3baf56f 100644 --- a/Makefile +++ b/Makefile @@ -5,22 +5,23 @@ clean: rm -f ./*.pyo cleantest: clean - mkdir -p keys - touch placeholder.log - rm -rf keys - rm *.log + mkdir -p gnupg/tests/keys + touch gnupg/tests/placeholder.log + rm -rf gnupg/tests/keys + rm gnupg/tests/*.log -test: cleantest - python test_gnupg.py basic +test: cleantest + python gnupg/tests/test_gnupg.py basic install: python setup.py install --record installed-files.txt uninstall: + touch installed-files.txt cat installed-files.txt | sudo xargs rm -rf clean-docs: - sphinx-apidoc -o docs -F -A "Isis Agora Lovecruft" -H "python-gnupg" -V 0.3.1 -R 0.3.1 . + sphinx-apidoc -F -A "Isis Agora Lovecruft" -H "python-gnupg" -V 0.4.0 -R 0.4.0 -o docs gnupg/ tests/ docs: cd docs From a4b8c7823fe08a6592d5360d374b84c032bfa21a Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 12 Apr 2013 00:02:06 +0000 Subject: [PATCH 068/397] Update Sphinx config due to directory layout change. --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 6f1b2a1..7704150 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,7 +16,7 @@ import sys, os # 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 # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('./../gnupg.py')) +sys.path.insert(0, os.path.abspath('./../gnupg')) sys.path.insert(0, os.path.abspath('.')) # -- Autodoc settings ---------------------------------------------------------- @@ -78,7 +78,7 @@ exclude_patterns = ['_build'] #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). From 85fc2692a545cb4b7ebd0cd134dfcc8ba81a6c17 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 12 Apr 2013 00:02:49 +0000 Subject: [PATCH 069/397] Change how we create the pubring.gpg and secring.gpg. --- gnupg/gnupg.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index cdfae42..ae7ac36 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -389,7 +389,7 @@ class GPG(object): for ring in [self.secring, self.pubring]: if ring and not os.path.isfile(ring): with open(ring, 'a+') as ringfile: - ringfile.write(" ") + ringfile.write("") ringfile.flush() try: assert _has_readwrite(ring), \ @@ -864,7 +864,7 @@ class GPG(object): parms.setdefault('Key-Type', 'RSA') parms.setdefault('Key-Length', 4096) parms.setdefault('Name-Real', "Autogenerated Key") - parms.setdefault('Name-Comment', "Generated by gnupg.py") + parms.setdefault('Name-Comment', "Generated by python-gnupg") try: logname = os.environ['LOGNAME'] except KeyError: @@ -875,8 +875,8 @@ class GPG(object): out = "Key-Type: %s\n" % parms.pop('Key-Type') for key, val in list(parms.items()): out += "%s: %s\n" % (key, val) - out += "%%pubring %s.pub\n" % self.pubring - out += "%%secring %s.pub\n" % self.secring + out += "%%pubring %s\n" % self.pubring + out += "%%secring %s\n" % self.secring out += "%commit\n" return out From 799845c760806ce8a1945b7488c990f9d126fbb7 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 14 Apr 2013 09:29:35 +0000 Subject: [PATCH 070/397] Remove the dash from "clean-docs" in the Makefile for tab completion. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 3baf56f..bb3f122 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ uninstall: touch installed-files.txt cat installed-files.txt | sudo xargs rm -rf -clean-docs: +cleandocs: sphinx-apidoc -F -A "Isis Agora Lovecruft" -H "python-gnupg" -V 0.4.0 -R 0.4.0 -o docs gnupg/ tests/ docs: From c99a8cb53e89feaf6ac8aaad2aa91b8b8ad92c1c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 14 Apr 2013 09:31:31 +0000 Subject: [PATCH 071/397] Remove autoclassing gnupg.GPG twice in the documentation. --- docs/gnupg.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/gnupg.rst b/docs/gnupg.rst index de78fdb..0b159f9 100644 --- a/docs/gnupg.rst +++ b/docs/gnupg.rst @@ -5,9 +5,3 @@ gnupg Module :members: :undoc-members: :show-inheritance: - - -GPG ---- - -.. autoclass:: gnupg.GPG From 5b0e8bc40799f252f9678c042989fdbe3d7907e7 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 14 Apr 2013 09:32:09 +0000 Subject: [PATCH 072/397] Add documentation for gnupg.parsers module. --- docs/index.rst | 1 + docs/parsers.rst | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 docs/parsers.rst diff --git a/docs/index.rst b/docs/index.rst index 90ab0a2..234f0ca 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,6 +12,7 @@ Contents: :maxdepth: 4 gnupg + parsers Indices and tables diff --git a/docs/parsers.rst b/docs/parsers.rst new file mode 100644 index 0000000..0b9c688 --- /dev/null +++ b/docs/parsers.rst @@ -0,0 +1,7 @@ +parsers Module +============== + +.. automodule:: parsers + :members: + :undoc-members: + :show-inheritance: From 7a688327e95feee3f9bb424e6970c0771f522a6d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:16:59 +0000 Subject: [PATCH 073/397] Add __init__ files for module directories. --- gnupg/__init__.py | 16 ++++++++++++++++ gnupg/tests/__init__.py | 0 2 files changed, 16 insertions(+) create mode 100644 gnupg/__init__.py create mode 100644 gnupg/tests/__init__.py diff --git a/gnupg/__init__.py b/gnupg/__init__.py new file mode 100644 index 0000000..ed10ed7 --- /dev/null +++ b/gnupg/__init__.py @@ -0,0 +1,16 @@ + +__author__ = 'Isis Agora Lovecruft' +__contact__ = 'isis@leap.se' +__date__ = '1 April 2013' +__url__ = 'https://github.com/isislovecruft/python-gnupg' +__version__ = '0.4.0' +__license__ = 'AGPLv3' + +import gnupg +from parsers import Crypt, DeleteResult, ListKeys +from parsers import GenKey, Sign, ImportResult, Verify +from gnupg import GPG + +__all__ = ["gnupg", + "Crypt", "DeleteResult", "ListKeys", + "GenKey", "Sign", "Encrypt", "ImportResult", "Verify"] diff --git a/gnupg/tests/__init__.py b/gnupg/tests/__init__.py new file mode 100644 index 0000000..e69de29 From c323935000d7a8bc5cbf6a2121905b3e0ae63a9a Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:18:30 +0000 Subject: [PATCH 074/397] Append genkey and sign tests to Makefile command. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index bb3f122..7b17d69 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ cleantest: clean rm gnupg/tests/*.log test: cleantest - python gnupg/tests/test_gnupg.py basic + python gnupg/tests/test_gnupg.py basic genkey sign install: python setup.py install --record installed-files.txt From 12a2a8fe354a6a2bf6679917540c947d840bc5ec Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:19:08 +0000 Subject: [PATCH 075/397] Add --no-site-files to virtualenv setup lne in Makefile. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7b17d69..7458256 100644 --- a/Makefile +++ b/Makefile @@ -29,4 +29,4 @@ docs: make html venv: - -source /usr/shared/python/ns/virtualenvwrapper.sh && mkvirtualenv -a "$PWD" --unzip-setuptools --distribute python-gnupg + -source /usr/shared/python/ns/virtualenvwrapper.sh && mkvirtualenv -a "$PWD" --no-site-packages --unzip-setuptools --distribute python-gnupg From 5a6414074bc205e2ab9641ace04269133e69dfec Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:20:07 +0000 Subject: [PATCH 076/397] Change __module__ level attributes. --- gnupg/gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index ae7ac36..d7e6672 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -75,10 +75,10 @@ Vinay Sajip's documentation: """ +__author__ = "Isis Agora Lovecruft" __module__ = 'gnupg' __version__ = "0.4.0" -__author__ = "Isis Agora Lovecruft" -__date__ = "12 Febuary 2013" + try: from io import StringIO From 4cf1c698c1be939e3de632040e6b7b73952497a5 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:22:41 +0000 Subject: [PATCH 077/397] Fix import statements in gnupg.py and util.py. --- gnupg/gnupg.py | 27 +++++++-------------------- gnupg/util.py | 40 ++++++++++++++++++++++++++++++---------- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index d7e6672..7f43a0c 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -86,7 +86,8 @@ try: except ImportError: from cStringIO import StringIO -from datetime import datetime +from subprocess import Popen +from subprocess import PIPE import codecs import locale @@ -94,30 +95,16 @@ import logging import os import re import socket -from subprocess import Popen -from subprocess import PIPE import sys import tempfile import threading -try: - import logging.NullHandler as NullHandler -except ImportError: - class NullHandler(logging.Handler): - def handle(self, record): - pass -finally: - logger = logging.getLogger(__module__) - if not logger.handlers: - logger.addHandler(NullHandler()) +from parsers import Verify, Crypt, DeleteResult, ImportResult +from parsers import GenKey, Sign, ListKeys, ListPackets +from parsers import _fix_unsafe, _sanitise, _is_allowed, _sanitise_list +from util import logger, _conf -try: - unicode - _py3k = False -except NameError: - _py3k = True - -ESCAPE_PATTERN = re.compile(r'\\x([0-9a-f][0-9a-f])', re.I) +import util def _copy_data(instream, outstream): diff --git a/gnupg/util.py b/gnupg/util.py index c64d4c5..de6156c 100644 --- a/gnupg/util.py +++ b/gnupg/util.py @@ -1,15 +1,35 @@ -""" -Utilities for Soledad. -""" +#!/usr/bin/env python +#-*- encoding: utf-8 -*- +# +# This file is part of python-gnupg, a Python wrapper around GnuPG. +# Copyright Ā© 2013 Isis Lovecruft, Andrej B. +# Ā© 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 Affero 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 +# GNU Affero General Public License for more details. +''' +utils.py +---------- +Extra utilities for python-gnupg. +''' +from gnupg import __author__ +from gnupg import __version__ +__module__ = 'gnupg.util' + +from datetime import datetime + +import logging import os -import gnupg -import re -from gnupg import ( - logger, - _is_sequence, - _make_binary_stream, -) class ListPackets(): From f6cd904708e7f551fb38d87eef168ca60d96704f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:23:48 +0000 Subject: [PATCH 078/397] Add note on android locale hack. --- gnupg/gnupg.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 7f43a0c..1f46e20 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -90,6 +90,9 @@ from subprocess import Popen from subprocess import PIPE import codecs +## For AOS, the locale module will need to point to a wrapper around the +## java.util.Locale class. +## See https://github.com/isislovecruft/android-locale-hack import locale import logging import os From 484ab3b44238ab832c77fe77d314a008f0528779 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:25:28 +0000 Subject: [PATCH 079/397] Update docstring for _copy_data(). --- gnupg/gnupg.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 1f46e20..3376ad6 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -111,11 +111,11 @@ import util def _copy_data(instream, outstream): - """ - Copy data from one stream to another. + """Copy data from one stream to another. - :param instream: A file descriptor to read from. - :param outstream: The file descriptor of a tmpfile to write to. + :type instream: :class:`io.BytesIO` or :class:`io.StringIO` or file + :param instream: A byte stream or open file to read from. + :param file outstream: The file descriptor of a tmpfile to write to. """ sent = 0 From a92490af2e797446766b5030459e78896342d433 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:28:22 +0000 Subject: [PATCH 080/397] Remove checks from gnupg.py and add utility functions to util.py. * Remove _has_readwrite() * Remove _is_file() * Remove _is_stream() * Remove _is_sequence() * Add _create_gpghome() * Add _find_gpgbinary() --- gnupg/gnupg.py | 39 ------------------------------- gnupg/util.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 39 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 3376ad6..79e5cf5 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -157,45 +157,6 @@ def _copy_data(instream, outstream): else: logger.debug("closed output, %d bytes sent", sent) -def _has_readwrite(path): - """ - Determine if the real uid/gid of the executing user has read and write - permissions for a directory or a file. - - :type path: C{str} - :param path: The path to the directory or file to check permissions for. - - :rtype: C{bool} - :param: True if real uid/gid has read+write permissions, False otherwise. - """ - return os.access(path, os.R_OK and os.W_OK) - -def _is_file(input): - """ - 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 - :func:`os.path.isfile`. - """ - try: - assert os.lstat(input).st_size > 0, "not a file: %s" % input - except (AssertionError, TypeError) as error: - logger.debug(error.message) - return False - else: - return True - -def _is_stream(input): - """Check that the input is a byte stream. - - :param input: An object provided for reading from or writing to - :rtype: C{bool} - :returns: True if :param:`input` is a stream, False if otherwise. - """ - return isinstance(input, BytesIO) - -def _is_sequence(instance): - return isinstance(instance,list) or isinstance(instance,tuple) - def _make_binary_stream(s, encoding): try: if _py3k: diff --git a/gnupg/util.py b/gnupg/util.py index de6156c..3b6cd17 100644 --- a/gnupg/util.py +++ b/gnupg/util.py @@ -31,6 +31,68 @@ from datetime import datetime import logging import os +try: + from io import StringIO + from io import BytesIO +except ImportError: + from cStringIO import StringIO + +try: + from logging import NullHandler +except: + class NullHandler(logging.Handler): + def handle(self, record): + pass +logger = logging.getLogger('gnupg') +if not logger.handlers: + logger.addHandler(NullHandler()) + +try: + unicode + _py3k = False +except NameError: + _py3k = True + +## Directory shortcuts: +_here = os.getcwd() ## .../python-gnupg/gnupg +_repo = _here.rsplit(__module__, 1)[0] ## .../python-gnupg +_test = os.path.join(_repo, 'tmp_test') ## .../python-gnupg/tmp_test +_user = os.environ.get('HOME') ## $HOME +_ugpg = os.path.join(_user, '.gnupg') ## $HOME/.gnupg +_conf = os.path.join(os.path.join(_user, '.config'), + 'python-gnupg') ## $HOME/.config/python-gnupg + +def _create_gpghome(gpghome): + """Create the specified GnuPG home directory, if necessary. + + :param str gpghome: The directory to use. + :rtype: bool + :returns: True if no errors occurred and the directory was created or + existed beforehand, False otherwise. + """ + ## xxx how will this work in a virtualenv? + if not os.path.isabs(gpghome): + message = ("Got non-abs gpg home dir path: %s" % gpghome) + logger.warn("util._create_gpghome(): %s" % message) + gpghome = os.path.abspath(gpghome) + if not os.path.isdir(gpghome): + message = ("Creating gpg home dir: %s" % gpghome) + logger.warn("util._create_gpghome(): %s" % message) + try: + os.makedirs(gpghome, 0x1C0) + except OSError as ose: + logger.error(ose, exc_info=1) + return False + else: + return True + else: + return True + +def _find_gpgbinary(gpgbinary=None): + """Find the absolute path to the GnuPG binary. + + Also run checks that the binary is not a symlink, and check that + our process real uid has exec permissions. class ListPackets(): """ From 425ca28a48d73de1367479f579ef5b9996b3f696 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:31:21 +0000 Subject: [PATCH 081/397] Update docstring for ListPackets class. --- gnupg/util.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gnupg/util.py b/gnupg/util.py index 3b6cd17..a9683c3 100644 --- a/gnupg/util.py +++ b/gnupg/util.py @@ -94,7 +94,11 @@ def _find_gpgbinary(gpgbinary=None): Also run checks that the binary is not a symlink, and check that our process real uid has exec permissions. -class ListPackets(): + :param str gpgbinary: The path to the GnuPG binary. + :raises: :exc:RuntimeError if it appears that GnuPG is not installed. + :rtype: str + :returns: The absolute path to the GnuPG binary to use, if no exceptions + occur. """ Handle status messages for --list-packets. """ From 2e6d34385b170e44afd2e994bc972769f56a76b9 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:33:17 +0000 Subject: [PATCH 082/397] Add checks to utils.py, update docstrings. * Add _create_gpghome() * Add _is_file() * Add _is_stream() * Add _is_list_or_tuple() * Add _today() * Add _which() * Update docstrings for methods in ListPackets. --- gnupg/util.py | 250 +++++++++++++++++++------------------------------- 1 file changed, 95 insertions(+), 155 deletions(-) diff --git a/gnupg/util.py b/gnupg/util.py index a9683c3..491bcab 100644 --- a/gnupg/util.py +++ b/gnupg/util.py @@ -100,174 +100,114 @@ def _find_gpgbinary(gpgbinary=None): :returns: The absolute path to the GnuPG binary to use, if no exceptions occur. """ - Handle status messages for --list-packets. + binary = None + if gpgbinary is not None: + if not os.path.isabs(gpgbinary): + try: binary = _which(gpgbinary)[0] + except IndexError as ie: logger.debug(ie.message) + if binary is None: + try: binary = _which('gpg')[0] + except IndexError: raise RuntimeError("gpg is not installed") + try: + assert os.path.isabs(binary), "Path to gpg binary not absolute" + assert not os.path.islink(binary), "Path to gpg binary is symlink" + assert os.access(binary, os.X_OK), "Lacking +x perms for gpg binary" + except (AssertionError, AttributeError) as ae: + logger.debug("util._find_gpgbinary(): %s" % ae.message) + else: + return binary + +def _has_readwrite(path): """ + Determine if the real uid/gid of the executing user has read and write + permissions for a directory or a file. - def __init__(self, gpg): - self.gpg = gpg - self.nodata = None - self.key = None - self.need_passphrase = None - self.need_passphrase_sym = None - self.userid_hint = None - - def handle_status(self, key, value): - # TODO: write tests for handle_status - if key == 'NODATA': - self.nodata = True - if key == 'ENC_TO': - # This will only capture keys in our keyring. In the future we - # may want to include multiple unknown keys in this list. - self.key, _, _ = value.split() - if key == 'NEED_PASSPHRASE': - self.need_passphrase = True - if key == 'NEED_PASSPHRASE_SYM': - self.need_passphrase_sym = True - if key == 'USERID_HINT': - self.userid_hint = value.strip().split() - - -class GPGWrapper(gnupg.GPG): + :param str path: The path to the directory or file to check permissions + for. + :rtype: bool + :returns: True if real uid/gid has read+write permissions, False otherwise. """ - This is a temporary class for handling GPG requests, and should be - replaced by a more general class used throughout the project. + return os.access(path, os.R_OK and os.W_OK) + +def _is_file(input): + """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 + :func:os.path.isfile. + + :param input: An object to check. + :rtype: bool + :returns: True if :param:input is file-like, False otherwise. """ + try: + assert os.lstat(input).st_size > 0, "not a file: %s" % input + except (AssertionError, TypeError) as error: + logger.debug(error.message) + return False + else: + return True - GNUPG_HOME = os.environ['HOME'] + "/.config/leap/gnupg" - GNUPG_BINARY = "/usr/bin/gpg" # this has to be changed based on OS +def _is_stream(input): + """Check that the input is a byte stream. - def __init__(self, gpgbinary=GNUPG_BINARY, gnupghome=GNUPG_HOME, - verbose=False, use_agent=False, keyring=None, options=None): - super(GPGWrapper, self).__init__(gnupghome=gnupghome, - gpgbinary=gpgbinary, - verbose=verbose, - use_agent=use_agent, - keyring=keyring, - options=options) - self.result_map['list-packets'] = ListPackets + :param input: An object provided for reading from or writing to. + :rtype: bool + :returns: True if :param:input is a stream, False if otherwise. + """ + return isinstance(input, BytesIO) or isinstance(input, StringIO) - def find_key_by_email(self, email, secret=False): - """ - Find user's key based on their email. - """ - for key in self.list_keys(secret=secret): - for uid in key['uids']: - if re.search(email, uid): - return key - raise LookupError("GnuPG public key for email %s not found!" % email) +def _is_list_or_tuple(instance): + """Check that ``instance`` is a list or tuple. - def find_key_by_subkey(self, subkey): - for key in self.list_keys(): - for sub in key['subkeys']: - if sub[0] == subkey: - return key - raise LookupError( - "GnuPG public key for subkey %s not found!" % subkey) + :param instance: The object to type check. + :rtype: bool + :returns: True if ``instance`` is a list or tuple, False otherwise. + """ + return isinstance(instance,list) or isinstance(instance,tuple) - def find_key_by_keyid(self, keyid): - for key in self.list_keys(): - if keyid == key['keyid']: - return key - raise LookupError( - "GnuPG public key for subkey %s not found!" % subkey) +## xxx unused function? +def _today(): + """Get the current date. - def encrypt(self, data, recipient, sign=None, always_trust=True, - passphrase=None, symmetric=False): - """ - Encrypt data using GPG. - """ - # TODO: devise a way so we don't need to "always trust". - return super(GPGWrapper, self).encrypt(data, recipient, sign=sign, - always_trust=always_trust, - passphrase=passphrase, - symmetric=symmetric, - cipher_algo='AES256') + :rtype: str + :returns: The date, in the format '%Y-%m-%d'. + """ + now_string = datetime.now().__str__() + return now_string.split(' ', 1)[0] - def decrypt(self, data, always_trust=True, passphrase=None): - """ - Decrypt data using GPG. - """ - # TODO: devise a way so we don't need to "always trust". - return super(GPGWrapper, self).decrypt(data, - always_trust=always_trust, - passphrase=passphrase) +def _which(executable, flags=os.X_OK): + """Borrowed from Twisted's :mod:twisted.python.proutils . - def send_keys(self, keyserver, *keyids): - """ - Send keys to a keyserver - """ - result = self.result_map['list'](self) - gnupg.logger.debug('send_keys: %r', keyids) - data = gnupg._make_binary_stream("", self.encoding) - args = ['--keyserver', keyserver, '--send-keys'] - args.extend(keyids) - self._handle_io(args, data, result, binary=True) - gnupg.logger.debug('send_keys result: %r', result.__dict__) - data.close() - return result + Search PATH for executable files with the given name. - def encrypt_file(self, file, recipients, sign=None, - always_trust=False, passphrase=None, - armor=True, output=None, symmetric=False, - cipher_algo=None): - "Encrypt the message read from the file-like object 'file'" - args = ['--encrypt'] - if symmetric: - args = ['--symmetric'] - if cipher_algo: - args.append('--cipher-algo %s' % cipher_algo) - else: - args = ['--encrypt'] - if not _is_sequence(recipients): - recipients = (recipients,) - for recipient in recipients: - args.append('--recipient "%s"' % recipient) - if armor: # create ascii-armored output - set to False for binary - args.append('--armor') - if output: # write the output to a file with the specified name - if os.path.exists(output): - os.remove(output) # to avoid overwrite confirmation message - args.append('--output "%s"' % output) - if sign: - args.append('--sign --default-key "%s"' % sign) - if always_trust: - args.append("--always-trust") - result = self.result_map['crypt'](self) - self._handle_io(args, file, result, passphrase=passphrase, binary=True) - logger.debug('encrypt result: %r', result.data) - return result + On newer versions of MS-Windows, the PATHEXT environment variable will be + set to the list of file extensions for files considered executable. This + will normally include things like ".EXE". This fuction will also find files + with the given name ending with any of these extensions. - def list_packets(self, raw_data): - args = ["--list-packets"] - result = self.result_map['list-packets'](self) - self._handle_io( - args, - _make_binary_stream(raw_data, self.encoding), - result, - ) - return result + On MS-Windows the only flag that has any meaning is os.F_OK. Any other + flags will be ignored. - def encrypted_to(self, raw_data): - """ - Return the key to which raw_data is encrypted to. - """ - # TODO: make this support multiple keys. - result = self.list_packets(raw_data) - if not result.key: - raise LookupError( - "Content is not encrypted to a GnuPG key!") - try: - return self.find_key_by_keyid(result.key) - except: - return self.find_key_by_subkey(result.key) + Note: This function does not help us prevent an attacker who can already + manipulate the environment's PATH settings from placing malicious code + higher in the PATH. It also does happily follows links. - def is_encrypted_sym(self, raw_data): - result = self.list_packets(raw_data) - return bool(result.need_passphrase_sym) - - def is_encrypted_asym(self, raw_data): - result = self.list_packets(raw_data) - return bool(result.key) - - def is_encrypted(self, raw_data): - self.is_encrypted_asym() or self.is_encrypted_sym() + :param str name: The name for which to search. + :param int flags: Arguments to L{os.access}. + :rtype: list + :returns: A list of the full paths to files found, in the order in which + they were found. + """ + result = [] + exts = filter(None, os.environ.get('PATHEXT', '').split(os.pathsep)) + path = os.environ.get('PATH', None) + if path is None: + return [] + for p in os.environ.get('PATH', '').split(os.pathsep): + p = os.path.join(p, executable) + if os.access(p, flags): + result.append(p) + for e in exts: + pext = p + e + if os.access(pext, flags): + result.append(pext) + return result From 48a7ad3374795136859d33f1f07ffbaf0cd69acd Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:39:01 +0000 Subject: [PATCH 083/397] Remove function _today() from gnupg.py. --- gnupg/gnupg.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 79e5cf5..7e03f06 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -171,11 +171,6 @@ def _make_binary_stream(s, encoding): rv = StringIO(s) return rv -def _today(): - """Get the current date as a string in the form %Y-%m-%d.""" - now_string = datetime.now().__str__() - return now_string.split(' ', 1)[0] - def _threaded_copy_data(instream, outstream): wr = threading.Thread(target=_copy_data, args=(instream, outstream)) wr.setDaemon(True) From d0783cb39826dce35c7286aa5cf202a17e4b25d6 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:40:28 +0000 Subject: [PATCH 084/397] Remove _which() from gnupg.py and add doctring for _threaded_copy_data(). --- gnupg/gnupg.py | 33 +++++---------------------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 7e03f06..d5b01b5 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -172,36 +172,13 @@ def _make_binary_stream(s, encoding): return rv def _threaded_copy_data(instream, outstream): - wr = threading.Thread(target=_copy_data, args=(instream, outstream)) - wr.setDaemon(True) - logger.debug('_threaded_copy_data(): %r, %r, %r', wr, instream, outstream) - wr.start() - return wr + """Copy data from one stream to another in a separate thread. -def _which(executable, flags=os.X_OK): - """Borrowed from Twisted's :mod:twisted.python.proutils . + Wraps ``_copy_data()`` in a :class:`threading.Thread`. - Search PATH for executable files with the given name. - - On newer versions of MS-Windows, the PATHEXT environment variable will be - set to the list of file extensions for files considered executable. This - will normally include things like ".EXE". This fuction will also find files - with the given name ending with any of these extensions. - - On MS-Windows the only flag that has any meaning is os.F_OK. Any other - flags will be ignored. - - Note: This function does not help us prevent an attacker who can already - manipulate the environment's PATH settings from placing malicious code - higher in the PATH. It also does happily follows links. - - :type name: C{str} - :param name: The name for which to search. - :type flags: C{int} - :param flags: Arguments to L{os.access}. - :rtype: C{list} - :param: A list of the full paths to files found, in the order in which - they were found. + :type instream: :class:`io.BytesIO` or :class:`io.StringIO` + :param instream: A byte stream to read from. + :param file outstream: The file descriptor of a tmpfile to write to. """ result = [] exts = filter(None, os.environ.get('PATHEXT', '').split(os.pathsep)) From a3e4cb661b6158456532ef87e3aee4e10dea8afd Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:41:04 +0000 Subject: [PATCH 085/397] Comment out faulting assertion to debug other issues. --- gnupg/gnupg.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index d5b01b5..8858c94 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -120,7 +120,8 @@ def _copy_data(instream, outstream): sent = 0 try: - assert _is_stream(instream), "instream is not a stream" + #assert (util._is_stream(instream) + # or isinstance(instream, file)), "instream not stream or file" assert isinstance(outstream, file), "outstream is not a file" except AssertionError as ae: logger.exception(ae) From 0a19ba201c4243748696be2016d6a2e46129fb98 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:41:50 +0000 Subject: [PATCH 086/397] Change _py3k check to belong in util.py --- gnupg/gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 8858c94..1b514db 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -160,7 +160,7 @@ def _copy_data(instream, outstream): def _make_binary_stream(s, encoding): try: - if _py3k: + if util._py3k: if isinstance(s, str): s = s.encode(encoding) else: From 0b54d68e6a3dbd8e517e1ae56a3d4672c60723b7 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:42:39 +0000 Subject: [PATCH 087/397] Refactor function _copy_data(). --- gnupg/gnupg.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 1b514db..5198595 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -181,20 +181,13 @@ def _threaded_copy_data(instream, outstream): :param instream: A byte stream to read from. :param file outstream: The file descriptor of a tmpfile to write to. """ - result = [] - exts = filter(None, os.environ.get('PATHEXT', '').split(os.pathsep)) - path = os.environ.get('PATH', None) - if path is None: - return [] - for p in os.environ.get('PATH', '').split(os.pathsep): - p = os.path.join(p, executable) - if os.access(p, flags): - result.append(p) - for e in exts: - pext = p + e - if os.access(pext, flags): - result.append(pext) - return result + copy_thread = threading.Thread(target=_copy_data, + args=(instream, outstream)) + copy_thread.setDaemon(True) + logger.debug('_threaded_copy_data(): %r, %r, %r', copy_thread, + instream, outstream) + copy_thread.start() + return copy_thread def _write_passphrase(stream, passphrase, encoding): passphrase = '%s\n' % passphrase From 273b5c8635125b915bc4076d7d65d757a2b41ff8 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:43:07 +0000 Subject: [PATCH 088/397] Make a logger statement more explicit. --- gnupg/gnupg.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 5198595..74118a3 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -193,7 +193,8 @@ def _write_passphrase(stream, passphrase, encoding): passphrase = '%s\n' % passphrase passphrase = passphrase.encode(encoding) stream.write(passphrase) - logger.debug("Wrote passphrase.") + logger.debug("_write_passphrase(): Wrote passphrase.") + class GPG(object): From 3f78e119eac1aa91fbacf0af71909eefaf5fd654 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:43:32 +0000 Subject: [PATCH 089/397] Remove extra whitespace line. --- gnupg/gnupg.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 74118a3..889c0a0 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -196,7 +196,6 @@ def _write_passphrase(stream, passphrase, encoding): logger.debug("_write_passphrase(): Wrote passphrase.") - class GPG(object): """Encapsulate access to the gpg executable""" decode_errors = 'strict' From ec1a81b1971ae84304e0a96431787412bd7d989a Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:44:18 +0000 Subject: [PATCH 090/397] Change GPG class attributes to be private and update docstring for GPG. --- gnupg/gnupg.py | 76 +++++++++++++++++++++----------------------------- 1 file changed, 32 insertions(+), 44 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 889c0a0..3c258fe 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -198,54 +198,42 @@ def _write_passphrase(stream, passphrase, encoding): class GPG(object): """Encapsulate access to the gpg executable""" - decode_errors = 'strict' + _decode_errors = 'strict' - result_map = {'crypt': Crypt, - 'delete': DeleteResult, - 'generate': GenKey, - 'import': ImportResult, - 'list': ListKeys, - 'sign': Sign, - 'verify': Verify,} + _result_map = {'crypt': Crypt, + 'delete': DeleteResult, + 'generate': GenKey, + 'import': ImportResult, + 'list': ListKeys, + 'sign': Sign, + 'verify': Verify,} - def __init__(self, gpgbinary=None, gpghome=None, - verbose=False, use_agent=False, - keyring=None, secring=None, pubring=None, + def __init__(self, gpgbinary=None, gpghome=None, verbose=False, + use_agent=False, keyring=None, secring=None, pubring=None, options=None): - """ - Initialize a GnuPG process wrapper. + """Initialize a GnuPG process wrapper. - :type gpgbinary: C{str} - :param gpgbinary: Name for GnuPG binary executable. If the absolute - path is not given, the evironment variable $PATH is - searched for the executable and checked that the - real uid/gid of the user has sufficient permissions. - :type gpghome: C{str} - :param gpghome: Full pathname to directory containing the public and - private keyrings. Default is whatever GnuPG defaults - to. - - :type keyring: C{str} - :param keyring: raises C{DeprecationWarning}. Use :param:pubring. - - :type secring: C{str} - :param secring: Name of alternative secret keyring file to use. If left - unspecified, this will default to using 'secring.gpg' - in the :param:gpghome directory, and create that file - if it does not exist. - - :type pubring: C{str} - :param pubring: Name of alternative public keyring file to use. If left - unspecified, this will default to using 'pubring.gpg' - in the :param:gpghome directory, and create that file - if it does not exist. - - :options: A list of additional options to pass to the GPG binary. - - :rtype: C{Exception} or C{} - :raises: RuntimeError with explanation message if there is a problem - invoking gpg. - :returns: + :param str gpgbinary: Name for GnuPG binary executable. If the absolute + path is not given, the evironment variable $PATH + is searched for the executable and checked that + the real uid/gid of the user has sufficient + permissions. + :param str gpghome: Full pathname to directory containing the public + and private keyrings. Default is whatever GnuPG + defaults to. + :param str keyring: raises :exc:DeprecationWarning. Use :param:pubring. + :param str secring: Name of alternative secret keyring file to use. If + left unspecified, this will default to using + 'secring.gpg' in the :param:gpghome directory, and + create that file if it does not exist. + :param str pubring: Name of alternative public keyring file to use. If + left unspecified, this will default to using + 'pubring.gpg' in the :param:gpghome directory, and + create that file if it does not exist. + :param list options: A list of additional options to pass to the GPG + binary. + :raises: :exc:`RuntimeError` with explanation message if there is a + problem invoking gpg. """ logger.warn("") From 92d52268ba05cf87a76039279c64af027e7f4a63 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:45:17 +0000 Subject: [PATCH 091/397] Make gpghome default to _conf directory from util.py. --- gnupg/gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 3c258fe..7197d9f 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -239,7 +239,7 @@ class GPG(object): logger.warn("") if not gpghome: - gpghome = os.path.join(os.getcwd(), 'gnupg') + gpghome = _conf self.gpghome = _fix_unsafe(gpghome) if self.gpghome: if not os.path.isdir(self.gpghome): From 93ab899d4a36f855a9e276c291add21f9096848f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:45:47 +0000 Subject: [PATCH 092/397] Use util._create_gpghome() function in GPG.__init__(). --- gnupg/gnupg.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 7197d9f..001118e 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -242,14 +242,7 @@ class GPG(object): gpghome = _conf self.gpghome = _fix_unsafe(gpghome) if self.gpghome: - if not os.path.isdir(self.gpghome): - message = ("Creating gpg home dir: %s" % gpghome) - logger.warn("GPG.__init__(): %s" % message) - os.makedirs(self.gpghome, 0x1C0) - if not os.path.isabs(self.gpghome): - message = ("Got non-abs gpg home dir path: %s" % self.gpghome) - logger.warn("GPG.__init__(): %s" % message) - self.gpghome = os.path.abspath(self.gpghome) + util._create_gpghome(self.gpghome) else: message = ("Unsuitable gpg home dir: %s" % gpghome) logger.debug("GPG.__init__(): %s" % message) From 5f966823787342ba0fe4f4a1d764cd92bda1fd65 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:46:32 +0000 Subject: [PATCH 093/397] Use util._find_gpgbinary() function in GPG.__init__(). --- gnupg/gnupg.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 001118e..89932d2 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -247,24 +247,7 @@ class GPG(object): message = ("Unsuitable gpg home dir: %s" % gpghome) logger.debug("GPG.__init__(): %s" % message) - ## find the absolute path, check that it is not a link, and check that - ## we have exec permissions - bin = None - if gpgbinary is not None: - if not os.path.isabs(gpgbinary): - try: bin = _which(gpgbinary)[0] - except IndexError as ie: logger.debug(ie.message) - if bin is None: - try: bin = _which('gpg')[0] - except IndexError: raise RuntimeError("gpg is not installed") - try: - assert os.path.isabs(bin), "Path to gpg binary not absolute" - assert not os.path.islink(bin), "Path to gpg binary is symbolic link" - assert os.access(bin, os.X_OK), "Lacking +x perms for gpg binary" - except (AssertionError, AttributeError) as ae: - logger.debug("GPG.__init__(): %s" % ae.message) - else: - self.gpgbinary = bin + self.gpgbinary = util._find_gpgbinary(gpgbinary) if keyring is not None: try: From 25a10567d06eda73d53cca8f4d4e98940dcc4bca Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:47:16 +0000 Subject: [PATCH 094/397] Raise a DeprecationWarning for GPG argument 'keyring'. --- gnupg/gnupg.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 89932d2..cead249 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -250,17 +250,10 @@ class GPG(object): self.gpgbinary = util._find_gpgbinary(gpgbinary) if keyring is not None: - try: - raise DeprecationWarning( - "Option 'keyring' changing to 'secring'") - except DeprecationWarning as dw: - log.warn(dw.message) - finally: - pubring = keyring + raise DeprecationWarning("Option 'keyring' changing to 'secring'") secring = 'secring.gpg' if secring is None else _fix_unsafe(secring) pubring = 'pubring.gpg' if pubring is None else _fix_unsafe(pubring) - self.secring = os.path.join(self.gpghome, secring) self.pubring = os.path.join(self.gpghome, pubring) ## XXX should eventually be changed throughout to 'secring', but until From 8f2204f464fa47bb25f45e37eada7b616f483deb Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:48:06 +0000 Subject: [PATCH 095/397] Don't touch non-existent pubring.gpg and secring.gpg, let GnuPG handle it. --- gnupg/gnupg.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index cead249..73a931c 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -256,20 +256,17 @@ class GPG(object): pubring = 'pubring.gpg' if pubring is None else _fix_unsafe(pubring) self.secring = os.path.join(self.gpghome, secring) self.pubring = os.path.join(self.gpghome, pubring) - ## XXX should eventually be changed throughout to 'secring', but until - ## then let's not break backward compatibility - self.keyring = self.pubring - for ring in [self.secring, self.pubring]: - if ring and not os.path.isfile(ring): - with open(ring, 'a+') as ringfile: - ringfile.write("") - ringfile.flush() - try: - assert _has_readwrite(ring), \ - ("Need r+w for %s" % ring) - except AssertionError as ae: - logger.debug(ae.message) + #for ring in [self.secring, self.pubring]: + # if ring and not os.path.isfile(ring): + # with open(ring, 'a+') as ringfile: + # ringfile.write("") + # ringfile.flush() + # try: + # assert util._has_readwrite(ring), \ + # ("Need r+w for %s" % ring) + # except AssertionError as ae: + # logger.debug(ae.message) self.options = _sanitise(options) if options else None From 4d74b5703f1497bef127f3afe835e77ccc4989c1 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:48:57 +0000 Subject: [PATCH 096/397] Remove resolved note on android locale. --- gnupg/gnupg.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 73a931c..83cbf73 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -270,7 +270,6 @@ class GPG(object): self.options = _sanitise(options) if options else None - ## xxx TODO: hack the locale module away so we can use this on android self.encoding = locale.getpreferredencoding() if self.encoding is None: # This happens on Jython! self.encoding = sys.stdin.encoding From bd98bc9003210093e57ec391a6d5dad4ed4bfd9a Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:50:08 +0000 Subject: [PATCH 097/397] Call check util._has_readwrite() from GPG.__init__(). --- gnupg/gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 83cbf73..21bc8df 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -276,8 +276,8 @@ class GPG(object): try: assert self.gpghome is not None, "Got None for self.gpghome" - assert _has_readwrite(self.gpghome), ("Home dir %s needs r+w" - % self.gpghome) + assert util._has_readwrite(self.gpghome), ("Home dir %s needs r+w" + % self.gpghome) assert self.gpgbinary, "Could not find gpgbinary %s" % full assert isinstance(verbose, bool), "'verbose' must be boolean" assert isinstance(use_agent, bool), "'use_agent' must be boolean" From 88a187bb0676c324d537c8a2dc2e9b4f1b70cf20 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:51:23 +0000 Subject: [PATCH 098/397] Update docstring for GPG._make_args(). --- gnupg/gnupg.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 21bc8df..67fa337 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -298,11 +298,11 @@ class GPG(object): raise RuntimeError("Error invoking gpg: %s: %s" % (proc.returncode, result.stderr)) - def make_args(self, args, passphrase=False): - """ - Make a list of command line elements for GPG. The value of ``args`` - will be appended. The ``passphrase`` argument needs to be True if - a passphrase will be sent to GPG, else False. + def _make_args(self, args, passphrase=False): + """Make a list of command line elements for GPG. The value of ``args`` + will be appended only if it passes the checks in + :func:parsers._sanitise. The ``passphrase`` argument needs to be True + if a passphrase will be sent to GPG, else False. """ cmd = [self.gpgbinary, '--status-fd 2 --no-tty'] if self.gpghome: From 41219d209c1a35fb1bda2dc7196a5c8b7f937f77 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:52:10 +0000 Subject: [PATCH 099/397] Add --secret-keyring to argument list in GPG._make_args(). --- gnupg/gnupg.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 67fa337..11a029c 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -307,9 +307,10 @@ class GPG(object): cmd = [self.gpgbinary, '--status-fd 2 --no-tty'] if self.gpghome: cmd.append('--homedir "%s"' % self.gpghome) - if self.keyring: - cmd.append('--no-default-keyring --keyring %s --secret-keyring %s' - % (self.pubring, self.secring)) + if self.pubring: + cmd.append('--no-default-keyring --keyring %s' % self.pubring) + if self.secring: + cmd.append('--secret-keyring %s' % self.secring) if passphrase: cmd.append('--batch --passphrase-fd 0') if self.use_agent: From 671b190a9172439da27779f5db06ae1bae89fd52 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:53:31 +0000 Subject: [PATCH 100/397] Update docstring for GPG._open_subprocess() and make _make_args() private. --- gnupg/gnupg.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 11a029c..737bf40 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -322,9 +322,10 @@ class GPG(object): return cmd def _open_subprocess(self, args=None, passphrase=False): - # Internal method: open a pipe to a GPG subprocess and return - # the file objects for communicating with it. - cmd = ' '.join(self.make_args(args, passphrase)) + """Open a pipe to a GPG subprocess and return the file objects for + communicating with it. + """ + cmd = ' '.join(self._make_args(args, passphrase)) if self.verbose: print(cmd) logger.debug("%s", cmd) From 9ce776116f109b49bc016f85efd67e037d94be46 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:54:25 +0000 Subject: [PATCH 101/397] Make a logger statement more explicit. --- gnupg/gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 737bf40..523fe43 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -328,7 +328,7 @@ class GPG(object): cmd = ' '.join(self._make_args(args, passphrase)) if self.verbose: print(cmd) - logger.debug("%s", cmd) + logger.debug("_open_subprocess(): %s", cmd) return Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE) def _read_response(self, stream, result): From d2b1ae62c2917edaabacf9cfb0a2898aaf3dc70f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:55:06 +0000 Subject: [PATCH 102/397] Update docstring for GPG._read_response(). --- gnupg/gnupg.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 523fe43..98db72d 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -332,11 +332,12 @@ class GPG(object): return Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE) def _read_response(self, stream, result): - # Internal method: reads all the stderr output from GPG, taking notice - # only of lines that begin with the magic [GNUPG:] prefix. - # - # Calls methods on the response object for each valid token found, - # with the arg being the remainder of the status line. + """Reads all the stderr output from GPG, taking notice only of lines + that begin with the magic [GNUPG:] prefix. + + Calls methods on the response object for each valid token found, with + the arg being the remainder of the status line. + """ lines = [] while True: line = stream.readline() From 6c0370efaa6045b34af05ac998f062ec06f33d00 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:55:45 +0000 Subject: [PATCH 103/397] Update docstrings for _read_data(), _handle_io(), and _collect_output(). --- gnupg/gnupg.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 98db72d..347b7f1 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -361,7 +361,7 @@ class GPG(object): result.stderr = ''.join(lines) def _read_data(self, stream, result): - # Read the contents of the file from GPG's stdout + """Read the contents of the file from GPG's stdout.""" chunks = [] while True: data = stream.read(1024) @@ -369,15 +369,14 @@ class GPG(object): break logger.debug("chunk: %r" % data[:256]) chunks.append(data) - if _py3k: + if util._py3k: # Join using b'' or '', as appropriate result.data = type(data)().join(chunks) else: result.data = ''.join(chunks) def _collect_output(self, process, result, writer=None, stdin=None): - """ - Drain the subprocesses output streams, writing the collected output + """Drain the subprocesses output streams, writing the collected output to the result. If a writer thread (writing to the subprocess) is given, make sure it's joined before returning. If a stdin stream is given, close it before returning. @@ -408,9 +407,7 @@ class GPG(object): stdout.close() def _handle_io(self, args, file, result, passphrase=False, binary=False): - """ - Handle a call to GPG - pass input data, collect output data. - """ + """Handle a call to GPG - pass input data, collect output data.""" p = self._open_subprocess(args, passphrase) if not binary: stdin = codecs.getwriter(self.encoding)(p.stdin) From 22e0443dcc79b76236c44dfd68595902f43baca2 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:56:49 +0000 Subject: [PATCH 104/397] Change GPG.sign() to handle files and strings, make _sign_file() private. --- gnupg/gnupg.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 347b7f1..66ed4cd 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -423,16 +423,27 @@ class GPG(object): # SIGNATURE METHODS # def sign(self, message, **kwargs): - """sign message""" - f = _make_binary_stream(message, self.encoding) - result = self.sign_file(f, **kwargs) - f.close() + """Create a signature for a message or file.""" + if isinstance(message, file): + result = self._sign_file(message, **kwargs) + elif not util._is_stream(message): + if isinstance(message, str): + if not util._py3k: + message = unicode(message, self.encoding) + message = message.encode(self.encoding) + f = _make_binary_stream(message, self.encoding) + result = self._sign_file(f, **kwargs) + f.close() + else: + logger.error("Unable to sign message '%s' with type %s" + % (message, type(message))) + result = None return result - def sign_file(self, file, keyid=None, passphrase=None, clearsign=True, - detach=False, binary=False): - """sign file""" - logger.debug("sign_file: %s", file) + def _sign_file(self, file, keyid=None, passphrase=None, clearsign=True, + detach=False, binary=False): + """Create a signature for a file.""" + logger.debug("GPG._sign_file(): %s", file) if binary: args = ['-s'] else: From f6e3d19826fe8003cefeb84a6988c4f3b036aa47 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:57:42 +0000 Subject: [PATCH 105/397] Change GnuPG argument construction in GPG.sign() to use allowed options. --- gnupg/gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 66ed4cd..ee9f37a 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -445,9 +445,9 @@ class GPG(object): """Create a signature for a file.""" logger.debug("GPG._sign_file(): %s", file) if binary: - args = ['-s'] + args = ['--sign'] else: - args = ['-sa'] + args = ['--sign --armor'] if clearsign: args.append("--clearsign") From 2691cc8d336cd53a2778d17d1e615fb1e7d1b4f0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:58:29 +0000 Subject: [PATCH 106/397] Change level of two log statements. --- gnupg/gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index ee9f37a..9411aad 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -452,9 +452,9 @@ class GPG(object): if clearsign: args.append("--clearsign") if detach: - logger.debug( + logger.warn( "Cannot use --clearsign and --detach-sign simultaneously.") - logger.debug( + logger.warn( "Using default GPG behaviour: --clearsign only.") elif detach and not clearsign: args.append("--detach-sign") From 265c32ac477e9c5e698dcd5288407c23ee9abc56 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:59:03 +0000 Subject: [PATCH 107/397] Change keyid in _sign_file() to remove extra quoting. --- gnupg/gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 9411aad..77bc59d 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -460,7 +460,7 @@ class GPG(object): args.append("--detach-sign") if keyid: - args.append('--default-key "%s"' % keyid) + args.append(str("--default-key %s" % keyid)) result = self.result_map['sign'](self) #We could use _handle_io here except for the fact that if the From 773b7adfb8d7e6397d5c86c9fe676ad2536328d2 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:59:31 +0000 Subject: [PATCH 108/397] Make _result_map a private attribute. --- gnupg/gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 77bc59d..555fcdd 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -292,7 +292,7 @@ class GPG(object): self.use_agent = use_agent proc = self._open_subprocess(["--version"]) - result = self.result_map['list'](self) + result = self._result_map['list'](self) self._collect_output(proc, result, stdin=proc.stdin) if proc.returncode != 0: raise RuntimeError("Error invoking gpg: %s: %s" @@ -462,7 +462,7 @@ class GPG(object): if keyid: args.append(str("--default-key %s" % keyid)) - result = self.result_map['sign'](self) + result = self._result_map['sign'](self) #We could use _handle_io here except for the fact that if the #passphrase is bad, gpg bails and you can't write the message. p = self._open_subprocess(args, passphrase is not None) From 4383e9ac5c01a2dd654b38542361ef7acf418e03 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 00:59:55 +0000 Subject: [PATCH 109/397] Update docstring for GPG.verify(). --- gnupg/gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 555fcdd..7b7ec99 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -478,7 +478,7 @@ class GPG(object): return result def verify(self, data): - """Verify the signature on the contents of the string 'data' + """Verify the signature on the contents of the string ``data``. >>> gpg = GPG(gpghome="keys") >>> input = gpg.gen_key_input(Passphrase='foo') From 7555cbac3cd1dccc9bafc79913e22c107cb84e53 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:01:02 +0000 Subject: [PATCH 110/397] Update docstring for GPG.verify_file(). --- gnupg/gnupg.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 7b7ec99..a1d9ed5 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -504,11 +504,11 @@ class GPG(object): signatures. If using detached signatures, the file containing the detached signature should be specified as the :param:data_filename. - :param file: A file descriptor object. Its type will be checked with - :func:_is_file. - :param data_filename: A file containing the GPG signature data for - :param:file. If given, :param:file is verified - via this detached signature. + :param file file: A file descriptor object. Its type will be checked + with :func:util._is_file. + :param file data_filename: A file containing the GPG signature data for + :param:file. If given, :param:file is + verified via this detached signature. """ ## attempt to wrap any escape characters in quotes: safe_file = _fix_unsafe(file) From 3626132c6c2f3f85676b0a8e717af54eace64860 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:01:35 +0000 Subject: [PATCH 111/397] Update check in GPG.verify_file() to use util._is_file(). --- gnupg/gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index a1d9ed5..fd46bef 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -513,8 +513,8 @@ class GPG(object): ## attempt to wrap any escape characters in quotes: safe_file = _fix_unsafe(file) - ## check that :param:`file` is actually a file: - _is_file(safe_file) + ## check that :param:file is actually a file: + util._is_file(safe_file) logger.debug('verify_file: %r, %r', safe_file, data_filename) result = self.result_map['verify'](self) From acbe787847dd0b68bba10f1a8babc95b64e569c9 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:02:44 +0000 Subject: [PATCH 112/397] Make _result_map attribute private. --- gnupg/gnupg.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index fd46bef..9e59207 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -517,7 +517,7 @@ class GPG(object): util._is_file(safe_file) logger.debug('verify_file: %r, %r', safe_file, data_filename) - result = self.result_map['verify'](self) + result = self._result_map['verify'](self) args = ['--verify'] if data_filename is None: self._handle_io(args, safe_file, result, binary=True) @@ -594,7 +594,7 @@ class GPG(object): ## xxx need way to validate that key_data is actually a valid GPG key ## it might be possible to use --list-packets and parse the output - result = self.result_map['import'](self) + result = self._result_map['import'](self) logger.debug('import_keys: %r', key_data[:256]) data = _make_binary_stream(key_data, self.encoding) self._handle_io(['--import'], data, result, binary=True) @@ -614,7 +614,7 @@ class GPG(object): """ safe_keyserver = _fix_unsafe(keyserver) - result = self.result_map['import'](self) + result = self._result_map['import'](self) data = _make_binary_stream("", self.encoding) args = ['--keyserver', keyserver, '--recv-keys'] @@ -637,7 +637,7 @@ class GPG(object): if _is_sequence(fingerprints): fingerprints = ' '.join(fingerprints) args = ['--batch --delete-%s "%s"' % (which, fingerprints)] - result = self.result_map['delete'](self) + result = self._result_map['delete'](self) p = self._open_subprocess(args) self._collect_output(p, result, stdin=p.stdin) return result @@ -654,7 +654,7 @@ class GPG(object): # gpg --export produces no status-fd output; stdout will be # empty in case of failure #stdout, stderr = p.communicate() - result = self.result_map['delete'](self) # any result will do + result = self._result_map['delete'](self) # any result will do self._collect_output(p, result, stdin=p.stdin) logger.debug('export_keys result: %r', result.data) return result.data.decode(self.encoding, self.decode_errors) From 1be0114847e3580f44738bc360c39d47295b5b57 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:06:29 +0000 Subject: [PATCH 113/397] Changes because of now-private attributes and checks moved to util.py. --- gnupg/gnupg.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 9e59207..8bacdb9 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -634,7 +634,7 @@ class GPG(object): which='key' if secret: which='secret-key' - if _is_sequence(fingerprints): + if util._is_list_or_tuple(fingerprints): fingerprints = ' '.join(fingerprints) args = ['--batch --delete-%s "%s"' % (which, fingerprints)] result = self._result_map['delete'](self) @@ -647,7 +647,7 @@ class GPG(object): which='' if secret: which='-secret-key' - if _is_sequence(keyids): + if util._is_list_or_tuple(keyids): keyids = ' '.join(['"%s"' % k for k in keyids]) args = ["--armor --export%s %s" % (which, keyids)] p = self._open_subprocess(args) @@ -657,7 +657,7 @@ class GPG(object): result = self._result_map['delete'](self) # any result will do self._collect_output(p, result, stdin=p.stdin) logger.debug('export_keys result: %r', result.data) - return result.data.decode(self.encoding, self.decode_errors) + return result.data.decode(self.encoding, self._decode_errors) def list_keys(self, secret=False): """List the keys currently in the keyring. @@ -687,10 +687,10 @@ class GPG(object): # ...nope, unless you care about expired sigs or keys (stevegt) # Get the response information - result = self.result_map['list'](self) + result = self._result_map['list'](self) self._collect_output(p, result, stdin=p.stdin) lines = result.data.decode(self.encoding, - self.decode_errors).splitlines() + self._decode_errors).splitlines() valid_keywords = 'pub uid sec fpr sub'.split() for line in lines: if self.verbose: From 26d8f98513209df3259867dcbe20f37f0e99446d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:07:22 +0000 Subject: [PATCH 114/397] Explicitly list public keys if we didn't specify for secret ones. --- gnupg/gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 8bacdb9..ff2b4d0 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -676,7 +676,7 @@ class GPG(object): """ - which='keys' + which='public-keys' if secret: which='secret-keys' args = "--list-%s --fixed-list-mode --fingerprint --with-colons" % (which,) From 5f3f26f91603d0964fdbce53c4fbbb22961cee7b Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:08:14 +0000 Subject: [PATCH 115/397] Refactor method GPG.gen_key(). --- gnupg/gnupg.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index ff2b4d0..d856d6f 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -720,11 +720,11 @@ class GPG(object): """ args = ["--gen-key --batch"] - result = self.result_map['generate'](self) + key = self._result_map['generate'](self) f = _make_binary_stream(input, self.encoding) - self._handle_io(args, f, result, binary=True) + self._handle_io(args, f, key, binary=True) f.close() - return result + return key def gen_key_input(self, **kwargs): """Generate GnuPG key(s) through batch file key generation. From 4a719cec791e93dc6356ed0cfaf7136dc34b80da Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:08:54 +0000 Subject: [PATCH 116/397] Update docstring for GPG.gen_key_input(). --- gnupg/gnupg.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index d856d6f..d3e2c87 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -731,8 +731,7 @@ class GPG(object): The GnuPG batch file key generation feature allows unattended key generation by creating a file with special syntax and then providing it - to: - $ gpg --gen-key --batch + to: gpg --gen-key --batch see http://www.gnupg.org/documentation/manuals/gnupg-devel/Unattended-GPG-key-generation.html#Unattended-GPG-key-generation for more details. From af996b8a698d067f6b0e90c37853787a4b801477 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:09:32 +0000 Subject: [PATCH 117/397] Update docstring for GPG.encrypt_file(). --- gnupg/gnupg.py | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index d3e2c87..7730169 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -786,9 +786,38 @@ class GPG(object): # ENCRYPTION # def encrypt_file(self, file, recipients, sign=None, - always_trust=False, passphrase=None, - armor=True, output=None, symmetric=False): - """Encrypt the message read from the file-like object :param:file .""" + always_trust=False, passphrase=None, + armor=True, output=None, symmetric=False): + """Encrypt the message read from ``file``. + + :type file: file or :class:BytesIO + :param file: The file or bytestream to encrypt. + :type recipients: str or list or tuple + :param recipients: The recipients to encrypt to. Recipients may be + specified by UID or keyID/fingerprint. + :param str sign: The keyID to use for signing, i.e. + "gpg --sign --default-key A3ADB67A2CDB8B35 ..." + :param bool always_trust: If True, ignore trust warnings on recipient + keys. If False, display trust warnings. + (default: False) + :param bool passphrase: If True, use the stored passphrase for our + secret key. + + :param bool armor: If True, ascii armor the encrypted output; if False, + the encrypted output will be in binary + format. (default: True) + + :param str output: The output file to write to. If not specified, the + encrypted output is returned, and thus should be + stored as an object in Python. For example: + + >>> gpg = gnupg.GPG(gpghome='./tmp_test') + + """ + if output: # write the output to a file with the specified name + if os.path.exists(output): + os.remove(output) # to avoid overwrite confirmation message + args.append('--output "%s"' % output) args = ['--encrypt'] if symmetric: args = ['--symmetric'] From a17b8828c1f3e2426c74c2c70bcf5a64170c0078 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:10:33 +0000 Subject: [PATCH 118/397] Again, _result_map is a private attribute. --- gnupg/gnupg.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 7730169..017b541 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -823,7 +823,7 @@ class GPG(object): args = ['--symmetric'] else: args = ['--encrypt'] - if not _is_sequence(recipients): + if not util._is_list_or_tuple(recipients): recipients = (recipients,) for recipient in recipients: args.append('--recipient "%s"' % recipient) @@ -837,7 +837,7 @@ class GPG(object): args.append('--sign --default-key "%s"' % sign) if always_trust: args.append("--always-trust") - result = self.result_map['crypt'](self) + result = self._result_map['crypt'](self) self._handle_io(args, file, result, passphrase=passphrase, binary=True) logger.debug('encrypt result: %r', result.data) return result @@ -916,7 +916,7 @@ class GPG(object): args.append('--output "%s"' % output) if always_trust: args.append("--always-trust") - result = self.result_map['crypt'](self) + result = self._result_map['crypt'](self) self._handle_io(args, file, result, passphrase, binary=True) logger.debug('decrypt result: %r', result.data) return result From 06a480b669b586e4998fb31293ca5cb2ef222fb7 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:11:20 +0000 Subject: [PATCH 119/397] Get rid of useless inline comment. --- gnupg/gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 017b541..966f504 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -827,7 +827,7 @@ class GPG(object): recipients = (recipients,) for recipient in recipients: args.append('--recipient "%s"' % recipient) - if armor: # create ascii-armored output - set to False for binary output + if armor: args.append('--armor') if output: # write the output to a file with the specified name if os.path.exists(output): From 4a63023e4b767931393ffebbd1f6aea4092a7fde Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:11:47 +0000 Subject: [PATCH 120/397] Update docstring for GPG.encrypt(). --- gnupg/gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 966f504..03bcb43 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -843,7 +843,7 @@ class GPG(object): return result def encrypt(self, data, recipients, **kwargs): - """Encrypt the message contained in the string :param:data . + """Encrypt the message contained in ``data`` to ``recipients``. >>> import shutil >>> if os.path.exists("keys"): From f357c16a21180df179e3e84bbd3f410cffd71d0d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:12:36 +0000 Subject: [PATCH 121/397] Move dreb's GPGWrapper class to gnupg.py. --- gnupg/gnupg.py | 142 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 03bcb43..b47da42 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -921,3 +921,145 @@ class GPG(object): logger.debug('decrypt result: %r', result.data) return result + +class GPGWrapper(GPG): + """ + This is a temporary class for handling GPG requests, and should be + replaced by a more general class used throughout the project. + """ + + def __init__(self, gpgbinary=None, gnupghome=_conf, + verbose=False, use_agent=False, keyring=None, options=None): + super(GPGWrapper, self).__init__(gnupghome=gnupghome, + gpgbinary=gpgbinary, + verbose=verbose, + use_agent=use_agent, + keyring=keyring, + options=options) + self._result_map['list-packets'] = ListPackets + + def find_key_by_email(self, email, secret=False): + """ + Find user's key based on their email. + """ + for key in self.list_keys(secret=secret): + for uid in key['uids']: + if re.search(email, uid): + return key + raise LookupError("GnuPG public key for email %s not found!" % email) + + def find_key_by_subkey(self, subkey): + for key in self.list_keys(): + for sub in key['subkeys']: + if sub[0] == subkey: + return key + raise LookupError( + "GnuPG public key for subkey %s not found!" % subkey) + + def find_key_by_keyid(self, keyid): + for key in self.list_keys(): + if keyid == key['keyid']: + return key + raise LookupError( + "GnuPG public key for subkey %s not found!" % subkey) + + def encrypt(self, data, recipient, sign=None, always_trust=True, + passphrase=None, symmetric=False): + """ + Encrypt data using GPG. + """ + # TODO: devise a way so we don't need to "always trust". + return super(GPGWrapper, self).encrypt(data, recipient, sign=sign, + always_trust=always_trust, + passphrase=passphrase, + symmetric=symmetric, + cipher_algo='AES256') + + def decrypt(self, data, always_trust=True, passphrase=None): + """ + Decrypt data using GPG. + """ + # TODO: devise a way so we don't need to "always trust". + return super(GPGWrapper, self).decrypt(data, + always_trust=always_trust, + passphrase=passphrase) + + def send_keys(self, keyserver, *keyids): + """ + Send keys to a keyserver + """ + result = self._result_map['list'](self) + gnupg.logger.debug('send_keys: %r', keyids) + data = gnupg._make_binary_stream("", self.encoding) + args = ['--keyserver', keyserver, '--send-keys'] + args.extend(keyids) + self._handle_io(args, data, result, binary=True) + gnupg.logger.debug('send_keys result: %r', result.__dict__) + data.close() + return result + + def encrypt_file(self, file, recipients, sign=None, + always_trust=False, passphrase=None, + armor=True, output=None, symmetric=False, + cipher_algo=None): + "Encrypt the message read from the file-like object 'file'" + args = ['--encrypt'] + if symmetric: + args = ['--symmetric'] + if cipher_algo: + args.append('--cipher-algo %s' % cipher_algo) + else: + args = ['--encrypt'] + if not util._is_list_or_tuple(recipients): + recipients = (recipients,) + for recipient in recipients: + args.append('--recipient "%s"' % recipient) + if armor: # create ascii-armored output - set to False for binary + args.append('--armor') + if output: # write the output to a file with the specified name + if os.path.exists(output): + os.remove(output) # to avoid overwrite confirmation message + args.append('--output "%s"' % output) + if sign: + args.append('--sign --default-key "%s"' % sign) + if always_trust: + args.append("--always-trust") + result = self._result_map['crypt'](self) + self._handle_io(args, file, result, passphrase=passphrase, binary=True) + logger.debug('encrypt result: %r', result.data) + return result + + def list_packets(self, raw_data): + args = ["--list-packets"] + result = self._result_map['list-packets'](self) + self._handle_io( + args, + _make_binary_stream(raw_data, self.encoding), + result, + ) + return result + + def encrypted_to(self, raw_data): + """ + Return the key to which raw_data is encrypted to. + """ + # TODO: make this support multiple keys. + result = self.list_packets(raw_data) + if not result.key: + raise LookupError( + "Content is not encrypted to a GnuPG key!") + try: + return self.find_key_by_keyid(result.key) + except: + return self.find_key_by_subkey(result.key) + + def is_encrypted_sym(self, raw_data): + result = self.list_packets(raw_data) + return bool(result.need_passphrase_sym) + + def is_encrypted_asym(self, raw_data): + result = self.list_packets(raw_data) + return bool(result.key) + + def is_encrypted(self, raw_data): + self.is_encrypted_asym() or self.is_encrypted_sym() From b09adb3f1901228231dd595682b71136c07129ec Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:13:47 +0000 Subject: [PATCH 122/397] Remove extra output handing logic in GPG.encrypt_file(). --- gnupg/gnupg.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index b47da42..6d384e9 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -829,10 +829,6 @@ class GPG(object): args.append('--recipient "%s"' % recipient) if armor: args.append('--armor') - if output: # write the output to a file with the specified name - if os.path.exists(output): - os.remove(output) # to avoid overwrite confirmation message - args.append('--output "%s"' % output) if sign: args.append('--sign --default-key "%s"' % sign) if always_trust: From 9c20a63a3618d324f1cf2939c0d0bd36f83fb54e Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:15:14 +0000 Subject: [PATCH 123/397] Add imports and module structure attributes to parsers.py. --- gnupg/parsers.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 4a2b29b..d9631ae 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -22,6 +22,19 @@ parsers.py Classes for parsing GnuPG status messages and sanitising commandline options. ''' +from gnupg import __author__ +from gnupg import __version__ +__module__ = 'gnupg.parsers' + +import logging +import re + +from util import logger +import util + + +ESCAPE_PATTERN = re.compile(r'\\x([0-9a-f][0-9a-f])', re.I) + class ProtectedOption(Exception): """Raised when the option passed to GPG is disallowed.""" From eea25b80b8ebe87d09456cbead12a001dfeac398 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:16:39 +0000 Subject: [PATCH 124/397] Avoid locally overriding builtins with variable name input. --- gnupg/parsers.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index d9631ae..62810fc 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -43,20 +43,20 @@ class UsageError(Exception): """Raised when incorrect usage of the API occurs..""" -def _fix_unsafe(input): +def _fix_unsafe(shell_input): """ Find characters used to escape from a string into a shell, and wrap them in quotes if they exist. Regex pilfered from python-3.x shlex module. - :param input: The input intended for the gnupg process. + :param str shell_input: The input intended for the GnuPG process. """ ## xxx do we want to add ';'? _unsafe = re.compile(r'[^\w@%+=:,./-]', 256) try: - if len(_unsafe.findall(input)) == 0: - return input + if len(_unsafe.findall(shell_input)) == 0: + return shell_input.strip() else: - clean = "'" + input.replace("'", "'\"'\"'") + "'" + clean = "'" + shell_input.replace("'", "'\"'\"'") + "'" return clean except TypeError: return None From 61c6b348db13e00f10f53f9b796d83556eb485fb Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:17:48 +0000 Subject: [PATCH 125/397] Update docstring for parsers._hyphenate(). --- gnupg/parsers.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 62810fc..b97bfa3 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -62,18 +62,13 @@ def _fix_unsafe(shell_input): return None def _hyphenate(input, add_prefix=False): - """ - Change underscores to hyphens so that object attributes can be easily + """Change underscores to hyphens so that object attributes can be easily tranlated to GPG option names. - :type input: C{str} - :param input: The attribute to hyphenate. - - :type add_prefix: C{bool} - :param add_prefix: If True, add leading hyphens to the input. - - :rtype: C{str} - :return: The :param:input with underscores changed to hyphens. + :param str input: The attribute to hyphenate. + :param bool add_prefix: If True, add leading hyphens to the input. + :rtype: str + :return: The ``input`` with underscores changed to hyphens. """ ret = '--' if add_prefix else '' ret += input.replace('_', '-') From 0b1f20c102508409f17e79be5c83498528279702 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:18:24 +0000 Subject: [PATCH 126/397] Update docstring for parsers._is_allowed(). --- gnupg/parsers.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index b97bfa3..593542b 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -80,25 +80,21 @@ def _is_allowed(input): options, the latter being a strict subset of the set of all options known to GPG. - :type input: C{str} - :param input: An input meant to be parsed as an option or flag to the GnuPG - process. Should be formatted the same as an option or flag - to the commandline gpg, i.e. "--encrypt-files". + :param str input: An input meant to be parsed as an option or flag to the + GnuPG process. Should be formatted the same as an option + or flag to the commandline gpg, i.e. "--encrypt-files". + :ivar frozenset _possible: All known GPG options and flags. + :ivar frozenset _allowed: All allowed GPG options and flags, e.g. all GPG + options and flags which we are willing to + acknowledge and parse. If we want to support a + new option, it will need to have its own parsing + class and its name will need to be added to this + set. - :type _possible: C{frozenset} - :ivar _possible: All known GPG options and flags. - - :type _allowed: C{frozenset} - :ivar _allowed: All allowed GPG options and flags, e.g. all GPG options and - flags which we are willing to acknowledge and parse. If we - want to support a new option, it will need to have its own - parsing class and its name will need to be added to this - set. - - :rtype: C{Exception} or C{str} - :raise: UsageError if :ivar:_allowed is not a subset of :ivar:_possible. - ProtectedOption if :param:input is not in the set :ivar:_allowed. - :return: The original parameter :param:input, unmodified and unsanitized, + :rtype: Exception or str + :raise: :exc:UsageError if ``_allowed`` is not a subset of ``_possible``. + ProtectedOption if ``input`` is not in the set ``_allowed``. + :return: The original parameter ``input``, unmodified and unsanitized, if no errors occur. """ From 72e03a961bd5ace33103a2b153a6cda07f819cb1 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:18:53 +0000 Subject: [PATCH 127/397] Allow options '--armor' and '--armour'. --- gnupg/parsers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 593542b..c0b7045 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -283,6 +283,7 @@ def _is_allowed(input): '--decrypt', '--decrypt-files', '--print-mds', '--print-md', '--sign', '--clearsign', '--detach-sign', + '--armor', '--armour', '--gen-key', '--batch', '--decrypt', '--decrypt-files', '--import', From a74a5ded9330f2a5a439e1649f8b8b1e255cae8c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:19:26 +0000 Subject: [PATCH 128/397] Allow option '--secret-keyring'. --- gnupg/parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index c0b7045..260ba9e 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -291,7 +291,7 @@ def _is_allowed(input): '--verify', '--version', '--output', '--status-fd', '--no-tty', '--passphrase-fd', - '--homedir', '--no-default-keyring', + '--homedir', '--no-default-keyring', '--default-key', '--keyring', '--secret-keyring', '--primary-keyring', '--fingerprint',]) From 394dca8ddcbd10bfd0b37cf854fef4696061fc83 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:19:49 +0000 Subject: [PATCH 129/397] Make a logger statement more specific. --- gnupg/parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 260ba9e..3d5d152 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -301,7 +301,7 @@ def _is_allowed(input): '_allowed is not subset of known options, difference: %s' \ % _allowed.difference(_possible) except AssertionError as ae: - logger.debug("gnupg._is_allowed(): %s" % ae.message) + logger.debug("_is_allowed(): %s" % ae.message) raise UsageError(ae.message) ## if we got a list of args, join them From 719a5068e9046226e5ad5ccf188d2809388a5c98 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:20:16 +0000 Subject: [PATCH 130/397] Update docstring for parsers._sanitise(). --- gnupg/parsers.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 3d5d152..bcbbee3 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -328,12 +328,11 @@ def _is_allowed(input): return None def _sanitise(*args): - """ - Take an arg or the key portion of a kwarg and check that it is in the set - of allowed GPG options and flags, and that it has the correct type. Then, - attempt to escape any unsafe characters. If an option is not allowed, - drop it with a logged warning. Returns a dictionary of all sanitised, - allowed options. + """Take an arg or the key portion of a kwarg and check that it is in the + set of allowed GPG options and flags, and that it has the correct + type. Then, attempt to escape any unsafe characters. If an option is not + allowed, drop it with a logged warning. Returns a dictionary of all + sanitised, allowed options. 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 From 13580ccac8c6d88e77737150d9bc567afb36643a Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:20:56 +0000 Subject: [PATCH 131/397] Remove excess whitespace. --- gnupg/parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index bcbbee3..7897226 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -281,7 +281,7 @@ def _is_allowed(input): '--delete-keys', '--delete-secret-keys', '--encrypt', '--encrypt-files', '--decrypt', '--decrypt-files', - '--print-mds', '--print-md', + '--print-mds', '--print-md', '--sign', '--clearsign', '--detach-sign', '--armor', '--armour', '--gen-key', '--batch', From 2a3849e9a38ccac972d6518763eea1866b9bb702 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:21:47 +0000 Subject: [PATCH 132/397] Update docstring for parsers._sanitise(). --- gnupg/parsers.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 7897226..b02d28b 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -348,11 +348,10 @@ def _sanitise(*args): If you're asking, "Is this *really* necessary?": No. Not really. See: https://xkcd.com/1181/ - :type args: C{str} - :param args: (optional) The boolean arguments which will be passed to the - GnuPG process. - :rtype: C{str} - :param: :ivar:sanitised + :param str args: (optional) The boolean arguments which will be passed to + the GnuPG process. + :rtype: str + :returns: ``sanitised`` """ def _check_arg_and_value(arg, value): From 1abffa966d1fb5539d6d4fa29ad77c0c32fe30e6 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:22:23 +0000 Subject: [PATCH 133/397] Update docstring for parsers._sanitise(). --- gnupg/parsers.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index b02d28b..1bf0aa8 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -354,25 +354,23 @@ def _sanitise(*args): :returns: ``sanitised`` """ - def _check_arg_and_value(arg, value): + def _check_option(arg, value): """ 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 :ivar:sanitised. - :type arg: C{str} - - :param arg: The arguments which will be passed to the GnuPG process, - and, optionally their corresponding values. The values are - any additional arguments following the GnuPG option or - flag. For example, if we wanted to pass "--encrypt - --recipient isis@leap.se" to gpg, then "--encrypt" would be - an arg without a value, and "--recipient" would also be an - arg, with a value of "isis@leap.se". - :type checked: C{list} - :ivar checked: The sanitised, allowed options and values. - :rtype: C{str} - :returns: A string of the items in :ivar:`checked` delimited by spaces. + :param str arg: The arguments which will be passed to the GnuPG + process, and, optionally their corresponding values. + The values are any additional arguments following the + GnuPG option or flag. For example, if we wanted to pass + "--encrypt --recipient isis@leap.se" to gpg, then + "--encrypt" would be an arg without a value, and + "--recipient" would also be an arg, with a value of + "isis@leap.se". + :ivar list checked: The sanitised, allowed options and values. + :rtype: str + :returns: A string of the items in ``checked`` delimited by spaces. """ safe_values = str() From c2fa9de80a3b42fcbcff0d30b11fa95ff3fff0f2 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:23:27 +0000 Subject: [PATCH 134/397] Refactor function parsers._sanitise(). --- gnupg/parsers.py | 89 ++++++++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 1bf0aa8..fdd182b 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -372,40 +372,35 @@ def _sanitise(*args): :rtype: str :returns: A string of the items in ``checked`` delimited by spaces. """ - safe_values = str() - + safe_option = str() try: - allowed_flag = _is_allowed(arg) - assert allowed_flag is not None, \ - "_check_arg_and_value(): got None for allowed_flag" + flag = _is_allowed(arg) + assert flag is not None, "_check_option(): got None for flag" except (AssertionError, ProtectedOption) as error: - logger.warn("_sanitise(): %s" % error.message) + logger.warn("_check_option(): %s" % error.message) else: - safe_values += (allowed_flag + " ") + safe_option += (flag + " ") if isinstance(value, str): - value_list = value.split(' ') - for value in value_list: - safe_value = _fix_unsafe(value) - if safe_value is not None and not safe_value.strip() == "": - if allowed_flag in ['--encrypt', '--encrypt-files', - '--decrypt', '--decrypt-file', - '--import', '--verify']: - ## Place checks here: - if _is_file(safe_value): - safe_values += (safe_value + " ") + values = value.split(' ') + for v in values: + val = _fix_unsafe(v) + if val is not None and val.strip() != "": + if flag in ['--encrypt', '--encrypt-files', '--decrypt', + '--decrypt-file', '--import', '--verify']: + ## Place checks here: + if _is_file(val): + safe_option += (val + " ") else: - logger.debug( - "_sanitize(): Option %s not file: %s" - % (allowed_flag, safe_value)) + logger.debug("_check_option(): %s not file: %s" + % (flag, val)) else: - safe_values += (safe_value + " ") - logger.debug( - "_sanitize(): No configured checks for: %s" - % safe_value) - return safe_values + safe_option += (val + " ") + logger.debug("_check_option(): No checks for %s" + % val) + return safe_option + is_flag = lambda x: x.startswith('-') checked = [] - if args is not None: for arg in args: if isinstance(arg, str): @@ -415,33 +410,37 @@ def _sanitise(*args): if arg.find(' ') > 0: filo = arg.split() filo.reverse() - is_flag = lambda x: x.startswith('-') new_arg, new_value = str(), str() while len(filo) > 0: - if is_flag(filo[0]): + if not is_flag(filo[0]): + logger.debug("_sanitise(): Got non-flag arg %s" + % filo[0]) + new_value += (filo.pop() + " ") + else: + logger.debug("_sanitise(): Got arg: %s" % filo[0]) new_arg = filo.pop() if len(filo) > 0: while not is_flag(filo[0]): - new_value += (filo.pop() + ' ') - else: - logger.debug("Got non-flag argument: %s" % filo[0]) - filo.pop() - safe = _check_arg_and_value(new_arg, new_value) - if safe is not None and not safe.strip() == '': - logger.debug("_sanitise(): appending args: %s" - % safe) - checked.append(safe) + logger.debug("_sanitise(): Got value: %s" + % filo[0]) + new_value += (filo.pop() + " ") + safe = _check_option(new_arg, new_value) + if safe is not None and not safe.strip() == "": + logger.debug("_sanitise(): appending option: %s" + % safe) + checked.append(safe) else: - safe = _check_arg_and_value(arg, None) - logger.debug("_sanitise(): appending args: %s" % safe) - checked.append(safe) - elif isinstance(arg, list): ## happens with '--version' - logger.debug("_sanitise(): Got arg list: %s" % arg) - for a in arg: - if a.startswith('--'): - safe = _check_arg_and_value(a, None) + safe = _check_option(arg, None) + if safe is not None: logger.debug("_sanitise(): appending args: %s" % safe) checked.append(safe) + else: + logger.debug("_sanitise(): got None for safe") + elif isinstance(arg, list): + logger.debug("_sanitise(): Got arg list: %s" % arg) + allow = _one_flag(arg) + if allow is not None: + checked.append(allow) else: logger.debug("_sanitise(): got non string or list arg: %s" % arg) From f1b5fbc19e4283516d4c81eb2b6c8dc230c43425 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:24:04 +0000 Subject: [PATCH 135/397] Update docstring for parsers._sanitise_list(). --- gnupg/parsers.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index fdd182b..a89b4a1 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -449,14 +449,13 @@ def _sanitise(*args): return sanitised def _sanitise_list(arg_list): - """ - A generator for running through a list of gpg options and sanitising them. + """A generator for iterating through a list of gpg options and sanitising + them. - :type arg_list: C{list} - :param arg_list: A list of options and flags for gpg. - :rtype: C{generator} + :param list arg_list: A list of options and flags for GnuPG. + :rtype: generator :return: A generator whose next() method returns each of the items in - :param:arg_list after calling :func:_sanitise with that item as a + ``arg_list`` after calling ``_sanitise()`` with that item as a parameter. """ if isinstance(arg_list, list): From 8133ff0cc32ec01b9e7530fe13aecc12d9367ee4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:24:51 +0000 Subject: [PATCH 136/397] Add Verify class to parsers.py. --- gnupg/parsers.py | 92 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index a89b4a1..faacf9e 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -465,6 +465,98 @@ def _sanitise_list(arg_list): yield safe_arg +class Verify(object): + """Parser for internal status messages from GnuPG for ``--verify``.""" + + TRUST_UNDEFINED = 0 + TRUST_NEVER = 1 + TRUST_MARGINAL = 2 + TRUST_FULLY = 3 + TRUST_ULTIMATE = 4 + + TRUST_LEVELS = { + "TRUST_UNDEFINED" : TRUST_UNDEFINED, + "TRUST_NEVER" : TRUST_NEVER, + "TRUST_MARGINAL" : TRUST_MARGINAL, + "TRUST_FULLY" : TRUST_FULLY, + "TRUST_ULTIMATE" : TRUST_ULTIMATE, + } + + def __init__(self, gpg): + self.gpg = gpg + self.valid = False + self.fingerprint = self.creation_date = self.timestamp = None + self.signature_id = self.key_id = None + self.username = None + self.status = None + self.pubkey_fingerprint = None + self.expire_timestamp = None + self.sig_timestamp = None + self.trust_text = None + self.trust_level = None + + def __nonzero__(self): + return self.valid + + __bool__ = __nonzero__ + + def handle_status(self, key, value): + if key in self.TRUST_LEVELS: + self.trust_text = key + self.trust_level = self.TRUST_LEVELS[key] + elif key in ("RSA_OR_IDEA", "NODATA", "IMPORT_RES", "PLAINTEXT", + "PLAINTEXT_LENGTH", "POLICY_URL", "DECRYPTION_INFO", + "DECRYPTION_OKAY", "INV_SGNR"): + pass + elif key == "BADSIG": + self.valid = False + self.status = 'signature bad' + self.key_id, self.username = value.split(None, 1) + elif key == "GOODSIG": + self.valid = True + self.status = 'signature good' + self.key_id, self.username = value.split(None, 1) + elif key == "VALIDSIG": + (self.fingerprint, + self.creation_date, + self.sig_timestamp, + self.expire_timestamp) = value.split()[:4] + # may be different if signature is made with a subkey + self.pubkey_fingerprint = value.split()[-1] + self.status = 'signature valid' + elif key == "SIG_ID": + (self.signature_id, + self.creation_date, self.timestamp) = value.split() + elif key == "ERRSIG": + self.valid = False + (self.key_id, + algo, hash_algo, + cls, + self.timestamp) = value.split()[:5] + self.status = 'signature error' + elif key == "DECRYPTION_FAILED": + self.valid = False + self.key_id = value + self.status = 'decryption failed' + elif key == "NO_PUBKEY": + self.valid = False + self.key_id = value + self.status = 'no public key' + elif key in ("KEYEXPIRED", "SIGEXPIRED"): + # these are useless in verify, since they are spit out for any + # pub/subkeys on the key, not just the one doing the signing. + # if we want to check for signatures with expired key, + # the relevant flag is EXPKEYSIG. + pass + elif key in ("EXPKEYSIG", "REVKEYSIG"): + # signed with expired or revoked key + self.valid = False + self.key_id = value.split()[0] + self.status = (('%s %s') % (key[:3], key[3:])).lower() + else: + raise ValueError("Unknown status message: %r" % key) + + class Crypt(Verify): """Handle status messages for --encrypt and --decrypt""" def __init__(self, gpg): From 03a9566e0ac12fba99eccdbb655280ad2e677f43 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:25:30 +0000 Subject: [PATCH 137/397] Update class Crypt in parsers.py. --- gnupg/parsers.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index faacf9e..66b7336 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -558,12 +558,14 @@ class Verify(object): class Crypt(Verify): - """Handle status messages for --encrypt and --decrypt""" + """Parser for internal status messages from GnuPG for ``--encrypt`` and + ``--decrypt``. + """ def __init__(self, gpg): - super(Crypt, self).__init__(self, gpg) - self.data = '' + super(Crypt, self).__init__(gpg) + self.data = str() self.ok = False - self.status = '' + self.status = str() def __nonzero__(self): if self.ok: return True @@ -577,7 +579,7 @@ class Crypt(Verify): def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. - :raises: :class:`ValueError` if the status message is unknown. + :raises: :exc:`ValueError` if the status message is unknown. """ if key in ("ENC_TO", "USERID_HINT", "GOODMDC", "END_DECRYPTION", "BEGIN_SIGNING", "NO_SECKEY", "ERROR", "NODATA", @@ -631,7 +633,7 @@ class GenKey(object): def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. - :raises: :class:`ValueError` if the status message is unknown. + :raises: :exc:`ValueError` if the status message is unknown. """ if key in ("PROGRESS", "GOOD_PASSPHRASE", "NODATA", "KEY_NOT_CREATED"): pass @@ -658,7 +660,7 @@ class DeleteResult(object): def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. - :raises: :class:`ValueError` if the status message is unknown. + :raises: :exc:`ValueError` if the status message is unknown. """ if key == "DELETE_PROBLEM": self.status = self.problem_reason.get(value, "Unknown error: %r" From aa96003b2666a8e8dbd3ef9594e5e97621ec2b4f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:26:01 +0000 Subject: [PATCH 138/397] Update docstrings for class Sign in parsers.py. --- gnupg/parsers.py | 117 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 95 insertions(+), 22 deletions(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 66b7336..5f47c2c 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -672,21 +672,33 @@ class Sign(object): """Parse GnuPG status messages for signing operations. :param gpg: An instance of :class:`gnupg.GPG`. - :type sig_type: :type:`str` - :attr sig_type: The type of signature created. - :type fingerprint: :type:`str` - :attr fingerprint: The fingerprint of the signing keyID. """ + #: The type of signature created. + sig_type = None + + #: The algorithm used to create the signature. + sig_algo = None + + #: The hash algorithm used to create the signature. + sig_hash_also = None + + #: The fingerprint of the signing keyid. + fingerprint = None + + #: The timestamp on the signature. + timestamp = None + + #: xxx fill me in + what = None + def __init__(self, gpg): self.gpg = gpg - self.sig_type = None - self.fingerprint = None def __nonzero__(self): """Override the determination for truthfulness evaluation. - :rtype: :type:`bool` + :rtype: bool :returns: True if we have a valid signature, False otherwise. """ return self.fingerprint is not None @@ -698,47 +710,108 @@ class Sign(object): def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. - :raises: :class:`ValueError` if the status message is unknown. + :raises: :exc:`ValueError` if the status message is unknown. """ if key in ("USERID_HINT", "NEED_PASSPHRASE", "BAD_PASSPHRASE", "GOOD_PASSPHRASE", "BEGIN_SIGNING", "CARDCTRL", "INV_SGNR", "NODATA"): pass elif key == "SIG_CREATED": - (self.sig_type, algo, hashalgo, cls, self.timestamp, - self.fingerprint) = value.split() + (self.sig_type, self.sig_algo, self.sig_hash_algo, + self.what, self.timestamp, self.fingerprint) = value.split() else: raise ValueError("Unknown status message: %r" % key) +class ListKeys(list): + """Handle status messages for --list-keys. + + Handle pub and uid (relating the latter to the former). + + Don't care about (info from src/DETAILS): + + crt = X.509 certificate + crs = X.509 certificate and private key available + ssb = secret subkey (secondary key) + uat = user attribute (same as user id except for field 10). + sig = signature + rev = revocation signature + pkd = public key data (special field format, see below) + grp = reserved for gpgsm + rvk = revocation key + """ + + def __init__(self, gpg): + super(ListKeys, self).__init__() + self.gpg = gpg + self.curkey = None + self.fingerprints = [] + self.uids = [] + + def key(self, args): + vars = (""" + type trust length algo keyid date expires dummy ownertrust uid + """).split() + self.curkey = {} + for i in range(len(vars)): + self.curkey[vars[i]] = args[i] + self.curkey['uids'] = [] + if self.curkey['uid']: + self.curkey['uids'].append(self.curkey['uid']) + del self.curkey['uid'] + self.curkey['subkeys'] = [] + self.append(self.curkey) + + pub = sec = key + + def fpr(self, args): + self.curkey['fingerprint'] = args[9] + self.fingerprints.append(args[9]) + + def uid(self, args): + uid = args[9] + uid = ESCAPE_PATTERN.sub(lambda m: chr(int(m.group(1), 16)), uid) + self.curkey['uids'].append(uid) + self.uids.append(uid) + + def sub(self, args): + subkey = [args[4], args[11]] + self.curkey['subkeys'].append(subkey) + + def handle_status(self, key, value): + pass + + class ImportResult(object): """Parse GnuPG status messages for key import operations. :type gpg: :class:`gnupg.GPG` :param gpg: An instance of :class:`gnupg.GPG`. - :type imported: :type:`list` - :attr imported: List of all keys imported. - :type fingerprints: :type:`list` - :attr fingerprints: A list of strings of the GnuPG keyIDs imported. - :type results: :type:`list` - :attr results: A list containing dictionaries with information gathered - on keys imported. """ counts = '''count no_user_id imported imported_rsa unchanged n_uids n_subk n_sigs n_revoc sec_read sec_imported sec_dups not_imported'''.split() + + #: List of all keys imported. + imported = list() + + #: A list of strings containing the fingerprints of the GnuPG keyIDs + #: imported. + fingerprints = list() + + #: A list containing dictionaries with information gathered on keys + #: imported. + results = list() + def __init__(self, gpg): self.gpg = gpg - self.imported = [] - self.results = [] - self.fingerprints = [] for result in self.counts: setattr(self, result, None) def __nonzero__(self): """Override the determination for truthfulness evaluation. - :rtype: :type:`bool` + :rtype: bool :returns: True if we have immport some keys, False otherwise. """ if self.not_imported: return False @@ -762,7 +835,7 @@ class ImportResult(object): def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. - :raises: :class:`ValueError` if the status message is unknown. + :raises: :exc:`ValueError` if the status message is unknown. """ if key == "IMPORTED": # this duplicates info we already see in import_ok & import_problem From 7eaffa884f8a9d3146e2ccbf692b3cb2f9e9cac7 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:26:43 +0000 Subject: [PATCH 139/397] Update class Verify docstrings in parsers.py. --- gnupg/parsers.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 5f47c2c..715a1aa 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -887,20 +887,15 @@ class Verify(object): :type gpg: :class:`gnupg.GPG` :param gpg: An instance of :class:`gnupg.GPG`. - :type valid: :type:`bool` - :attr valid: True if the signature or file was verified successfully, - False otherwise. - :type fingerprint: :type:`str` - :attr fingerprint: The fingerprint of the GnuPG keyID which created the - signature. - :type creation_date: :type:`str` - :attr creation_date: The date the signature was made. - :type timestamp: :type:`str` - :attr timestamp: The timestamp used internally in the signature. - :type signature_id: :type:`str` - :attr signature_id: The uid of the signing GnuPG key. - :type status: :type:`str` - :attr status: The internal status message from the GnuPG process. + :attr bool valid: True if the signature or file was verified successfully, + False otherwise. + :attr str fingerprint: The fingerprint of the GnuPG keyID which created the + signature. + + :attr str creation_date: The date the signature was made. + :attr str timestamp: The timestamp used internally in the signature. + :attr str signature_id: The uid of the signing GnuPG key. + :attr str status: The internal status message from the GnuPG process. """ ## xxx finish documentation @@ -932,7 +927,7 @@ class Verify(object): def __nonzero__(self): """Override the determination for truthfulness evaluation. - :rtype: :type bool: + :rtype: bool :returns: True if we have a valid signature, False otherwise. """ return self.valid @@ -941,7 +936,7 @@ class Verify(object): def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. - :raises: :class:`ValueError` if the status message is unknown. + :raises: :exc:`ValueError` if the status message is unknown. """ if key in self.TRUST_LEVELS: self.trust_text = key From c01038df43d1b7f1e88fd0fd9f67ce4c2e79baed Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:27:04 +0000 Subject: [PATCH 140/397] Add dreb's ListPackets class to parsers.py. --- gnupg/parsers.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 715a1aa..9bd8a68 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -992,3 +992,39 @@ class Verify(object): self.status = (('%s %s') % (key[:3], key[3:])).lower() else: raise ValueError("Unknown status message: %r" % key) + +## xxx old style class + +class ListPackets(): + """ + Handle status messages for --list-packets. + """ + + def __init__(self, gpg): + self.gpg = gpg + self.nodata = None + self.key = None + self.need_passphrase = None + self.need_passphrase_sym = None + self.userid_hint = None + + def handle_status(self, key, value): + """Parse a status code from the attached GnuPG process. + + :raises: :exc:`ValueError` if the status message is unknown. + """ + # TODO: write tests for handle_status + if key == 'NODATA': + self.nodata = True + elif key == 'ENC_TO': + # This will only capture keys in our keyring. In the future we + # may want to include multiple unknown keys in this list. + self.key, _, _ = value.split() + elif key == 'NEED_PASSPHRASE': + self.need_passphrase = True + elif key == 'NEED_PASSPHRASE_SYM': + self.need_passphrase_sym = True + elif key == 'USERID_HINT': + self.userid_hint = value.strip().split() + else: + raise ValueError("Unknown status message: %r" % key) From aaa492b75b7f03403d21d67453cc6c34c05357c6 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:28:25 +0000 Subject: [PATCH 141/397] Update assertions and docstrings for unittests. --- gnupg/tests/test_gnupg.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index cbcbec8..a2208ee 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -25,6 +25,7 @@ else: import unittest import gnupg +from gnupg import parsers __author__ = gnupg.__author__ __date__ = gnupg.__date__ @@ -42,7 +43,7 @@ def _make_tempfile(*args, **kwargs): return tempfile.TemporaryFile(dir=tempfile.gettempdir(), *args, **kwargs) -logger = logging.getLogger(gnupg.logger.name) +logger = logging.getLogger('gnupg') KEYS_TO_IMPORT = """-----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1.4.9 (MingW32) @@ -104,9 +105,7 @@ def is_list_with_len(o, n): return isinstance(o, list) and len(o) == n def compare_keys(k1, k2): - """ - Compare ASCII keys. - """ + """Compare ASCII keys.""" k1 = k1.split('\n') k2 = k2.split('\n') del k1[1] # remove version lines @@ -123,9 +122,7 @@ class ResultStringIO(io.StringIO): class GPGTestCase(unittest.TestCase): - """ - A group of :class:`unittest.TestCase` unittests for testing python-gnupg. - """ + """:class:`unittest.TestCase `s for python-gnupg.""" @classmethod def setUpClass(cls): @@ -138,9 +135,7 @@ class GPGTestCase(unittest.TestCase): pass def setUp(self): - """ - This method is called once per self.test_* method. - """ + """This method is called once per self.test_* method.""" hd = HOME_DIR if os.path.exists(hd): self.assertTrue(os.path.isdir(hd), "Not a directory: %s" % hd) @@ -317,13 +312,11 @@ class GPGTestCase(unittest.TestCase): """ key = self.generate_key("OMG Moar Coffee", "giveitto.me", subkey_type='ELG-E') - self.assertIsNotNone(key) + self.assertIsNotNone(key.type) self.assertIsNotNone(key.fingerprint) def test_key_generation_with_invalid_key_type(self): - """ - Test that key generation handles invalid key type. - """ + """Test that key generation handles invalid key type.""" params = { 'Key-Type': 'INVALID', 'Key-Length': 1024, From 998a9b8f88de92df340251569507a67215f3e28d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:29:44 +0000 Subject: [PATCH 142/397] Update and add new uniitests. --- gnupg/tests/test_gnupg.py | 254 +++++++++++++++++++++----------------- 1 file changed, 143 insertions(+), 111 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index a2208ee..a3612f6 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -145,39 +145,42 @@ class GPGTestCase(unittest.TestCase): self.pubring = os.path.join(self.homedir, 'pubring.gpg') self.secring = os.path.join(self.homedir, 'secring.gpg') - def test_environment(self): - """ - Test the environment by ensuring that setup worked. - """ + def test_gpghome_creation(self): + """Test the environment by ensuring that setup worked.""" hd = self.homedir self.assertTrue(os.path.exists(hd) and os.path.isdir(hd), "Not an existing directory: %s" % hd) def test_gpg_binary(self): - """ - Test that 'gpg --version' does not return an error code. - """ + """Test that 'gpg --version' does not return an error code.""" proc = self.gpg._open_subprocess(['--version']) result = io.StringIO() self.gpg._collect_output(proc, result, stdin=proc.stdin) self.assertEqual(proc.returncode, 0) def test_gpg_binary_version_str(self): - """ - That that 'gpg --version' returns the expected output. - """ + """That that 'gpg --version' returns the expected output.""" proc = self.gpg._open_subprocess(['--version']) result = proc.stdout.read(1024) expected1 = "Supported algorithms:" expected2 = "Pubkey:" expected3 = "Cipher:" expected4 = "Compression:" - logger.debug("'gpg --version' returned output:n%s" % result) + #logger.debug("'gpg --version' returned output:n%s" % result) self.assertGreater(result.find(expected1), 0) self.assertGreater(result.find(expected2), 0) self.assertGreater(result.find(expected3), 0) self.assertGreater(result.find(expected4), 0) + def test_gpg_binary_not_installed(self): + """Test that Gnupg installation can be detected.""" + env_copy = os.environ + path_copy = os.environ.pop('PATH') + with self.assertRaises(RuntimeError): + gnupg.GPG(gpghome=self.homedir) + os.environ = env_copy + os.environ.update({'PATH': path_copy}) + def test_gpg_binary_not_abs(self): """Test that a non-absolute path to gpg results in a full path.""" self.assertTrue(os.path.isabs(self.gpg.gpgbinary)) @@ -186,20 +189,20 @@ class GPGTestCase(unittest.TestCase): """Test that unsupported gpg options are dropped.""" self.gpg.options = ['--tyrannosaurus-rex', '--stegosaurus'] self.gpg.keyring = self.secring - cmd = self.gpg.make_args(None, False) + cmd = self.gpg._make_args(None, False) expected = ['/usr/bin/gpg', '--status-fd 2 --no-tty', '--homedir "%s"' % os.path.join(os.getcwd(), 'keys'), - '--no-default-keyring --keyring %s --secret-keyring %s' - % (self.pubring, self.secring)] + '--no-default-keyring --keyring %s' % self.pubring, + '--secret-keyring %s' % self.secring] self.assertListEqual(cmd, expected) def test_make_args(self): """Test argument line construction.""" not_allowed = ['--bicycle', '--zeppelin', 'train', 'flying-carpet'] self.gpg.options = not_allowed[:-2] - args = self.gpg.make_args(not_allowed[2:], False) - self.assertTrue(len(args) == 4) + args = self.gpg._make_args(not_allowed[2:], False) + self.assertTrue(len(args) == 5) for na in not_allowed: self.assertNotIn(na, args) @@ -233,10 +236,8 @@ class GPGTestCase(unittest.TestCase): """Generate a GnuPG batch file for key unattended key creation.""" name = real_name.lower().replace(' ', '') - ## XXX will GPG just use it's defaults? does it have defaults if - ## we've just given it a homedir without a gpg.conf? key_type = 'RSA'if key_type is None else key_type - key_length = 4096 if key_length is None else key_length + key_length = 1024 if key_length is None else key_length batch = {'Key-Type': key_type, 'Key-Length': key_length, @@ -258,11 +259,14 @@ class GPGTestCase(unittest.TestCase): """Generate a basic key.""" key_input = self.generate_key_input(real_name, email_domain, **kwargs) key = self.gpg.gen_key(key_input) + print "\nKEY TYPE: ", key.type + print "KEY FINGERPRINT: ", key.fingerprint + return key def test_gen_key_input(self): """Test that GnuPG batch file creation is successful.""" key_input = self.generate_key_input("Francisco Ferrer", "an.ok") - self.assertIsNotNone(key_input) + self.assertIsInstance(key_input, str) self.assertGreater(key_input.find('Francisco Ferrer'), 0) def test_rsa_key_generation(self): @@ -270,7 +274,7 @@ class GPGTestCase(unittest.TestCase): Test that RSA key generation succeeds. """ key = self.generate_key("Barbara Brown", "beta.com") - self.assertIsNotNone(key) + self.assertIsNotNone(key.type) self.assertIsNotNone(key.fingerprint) def test_rsa_key_generation_with_unicode(self): @@ -278,7 +282,7 @@ class GPGTestCase(unittest.TestCase): Test that RSA key generation succeeds with unicode characters. """ key = self.generate_key("AnaĆÆs de Flavigny", "ĆŖtrerien.fr") - self.assertIsNotNone(key) + self.assertIsNotNone(key.type) self.assertIsNotNone(key.fingerprint) def test_rsa_key_generation_with_subkey(self): @@ -287,7 +291,7 @@ class GPGTestCase(unittest.TestCase): """ key = self.generate_key("Need Caffeine", "nowplea.se", subkey_type='RSA') - self.assertIsNotNone(key) + self.assertIsNotNone(key.type) self.assertIsNotNone(key.fingerprint) def test_dsa_key_generation(self): @@ -295,7 +299,7 @@ class GPGTestCase(unittest.TestCase): Test that DSA key generation succeeds. """ key = self.generate_key("DSA Signonly", "test.com") - self.assertIsNotNone(key) + self.assertIsNotNone(key.type) self.assertIsNotNone(key.fingerprint) def test_dsa_key_generation_with_unicode(self): @@ -303,7 +307,7 @@ class GPGTestCase(unittest.TestCase): Test that DSA key generation succeeds with unicode characters. """ key = self.generate_key("ē “å£ŠåˆčØˆć™ć‚‹", "ē “å£ŠåˆčØˆć™ć‚‹.ę—„ęœ¬") - self.assertIsNotNone(key) + self.assertIsNotNone(key.type) self.assertIsNotNone(key.fingerprint) def test_dsa_key_generation_with_subkey(self): @@ -329,14 +333,11 @@ class GPGTestCase(unittest.TestCase): } batch = self.gpg.gen_key_input(**params) key = self.gpg.gen_key(batch) - self.assertIsInstance(key.data, str) - self.assertEquals(key.data, '') - self.assertIs(None, key.fingerprint, 'Null fingerprint result') + self.assertIsNone(key.type) + self.assertIsNone(key.fingerprint) def test_key_generation_with_colons(self): - """ - Test that key generation handles colons in Name fields. - """ + """Test that key generation handles colons in Name fields.""" params = { 'key_type': 'RSA', 'name_real': 'urn:uuid:731c22c4-830f-422f-80dc-14a9fdae8c19', @@ -344,14 +345,12 @@ class GPGTestCase(unittest.TestCase): 'name_email': 'test.name@example.com', } batch = self.gpg.gen_key_input(**params) - key = self.gpg.gen_key(cmd) - print "KEY DATA\n", key.data - print "KEY FINGERPRINT\n", key.fingerprint + key = self.gpg.gen_key(batch) + self.assertIsNotNone(key.type) + self.assertIsNotNone(key.fingerprint) def test_key_generation_import_list_with_colons(self): - """ - Test that key generation handles colons in Name fields. - """ + """Test that key generation handles colons in Name fields.""" params = { 'key_type': 'RSA', 'name_real': 'urn:uuid:731c22c4-830f-422f-80dc-14a9fdae8c19', @@ -359,11 +358,14 @@ class GPGTestCase(unittest.TestCase): 'name_email': 'test.name@example.com', } batch = self.gpg.gen_key_input(**params) - key = self.gpg.gen_key(cmd) + self.assertIsInstance(batch, str) + key = self.gpg.gen_key(batch) keys = self.gpg.list_keys() self.assertIsNotNone(key) self.assertEqual(len(keys), 1) key = keys[0] + self.assertIsNotNone(key.type) + self.assertIsNotNone(key.fingerprint) uids = key['uids'] self.assertEqual(len(uids), 1) uid = uids[0] @@ -371,24 +373,20 @@ class GPGTestCase(unittest.TestCase): '(dummy comment) ') def test_key_generation_with_empty_value(self): - """ - Test that key generation handles empty values. - """ - params = { - 'key_type': 'RSA', - 'key_length': 1024, - 'name_comment': ' ', # Not added, so default will appear - } - cmd = self.gpg.gen_key_input(**params) - self.assertTrue('\nName-Comment: Generated by gnupg.py\n' in cmd) - params['name_comment'] = 'A' - cmd = self.gpg.gen_key_input(**params) - self.assertTrue('\nName-Comment: A\n' in cmd) + """Test that key generation handles empty values.""" + params = {'name_comment': ' '} + batch = self.gpg.gen_key_input(**params) + self.assertTrue('\nName-Comment: Generated by python-gnupg\n' in batch) + + def test_key_generation_override_default_value(self): + """Test that overriding a default value in gen_key_input() works.""" + params = {'name_comment': 'A'} + batch = self.gpg.gen_key_input(**params) + self.assertFalse('\nName-Comment: Generated by python-gnupg\n' in batch) + self.assertTrue('\nName-Comment: A\n' in batch) def test_list_keys_after_generation(self): - """ - Test that after key generation, the generated key is available. - """ + """Test that after key generation, the generated key is available.""" self.test_list_keys_initial() self.do_key_generation() public_keys = self.gpg.list_keys() @@ -399,14 +397,12 @@ class GPGTestCase(unittest.TestCase): "1-element list expected") def test_encryption_and_decryption(self): - """ - Test that encryption and decryption works. - """ + """Test that encryption and decryption works.""" logger.debug("test_encryption_and_decryption begins") - key = self.generate_key("Andrew", "Able", "alpha.com", + key = self.generate_key("Andrew Able", "alpha.com", passphrase="andy") andrew = key.fingerprint - key = self.generate_key("Barbara", "Brown", "beta.com") + key = self.generate_key("Barbara Brown", "beta.com") barbara = key.fingerprint gpg = self.gpg gpg.encoding = 'latin-1' @@ -431,28 +427,23 @@ class GPGTestCase(unittest.TestCase): logger.debug("test_encryption_and_decryption ends") # Test symmetric encryption data = "chippy was here" - edata = str(gpg.encrypt(data, None, passphrase='bbrown', symmetric=True)) + edata = str(gpg.encrypt(data, None, passphrase='bbrown', + symmetric=True)) ddata = gpg.decrypt(edata, passphrase='bbrown') self.assertEqual(data, str(ddata)) def test_public_keyring(self): - """ - Test that the public keyring is found in the gpg home directory. - """ + """Test that the public keyring is found in the gpg home directory.""" self.gpg.keyring = self.pubring self.assertTrue(os.path.isfile(self.pubring)) def test_secret_keyring(self): - """ - Test that the secret keyring is found in the gpg home directory. - """ + """Test that the secret keyring is found in the gpg home directory.""" self.gpg.keyring = self.secring self.assertTrue(os.path.isfile(self.secring)) def test_import_and_export(self): - """ - Test that key import and export works. - """ + """Test that key import and export works.""" logger.debug("test_import_and_export begins") self.test_list_keys_initial() gpg = self.gpg @@ -481,9 +472,7 @@ class GPGTestCase(unittest.TestCase): logger.debug("test_import_and_export ends") def test_import_only(self): - """ - Test that key import works. - """ + """Test that key import works.""" logger.debug("test_import_only begins") self.test_list_keys_initial() self.gpg.import_keys(KEYS_TO_IMPORT) @@ -504,23 +493,63 @@ class GPGTestCase(unittest.TestCase): self.assertEqual(0, match, "Keys must match") logger.debug("test_import_only ends") - def test_signature_verification(self): - """ - Test that signing and verification works. - """ - logger.debug("test_signature_verification begins") - key = self.generate_key("Andrew", "Able", "alpha.com") - self.gpg.encoding = 'latin-1' - if gnupg._py3k: - data = 'Hello, AndrĆ©!' - else: - data = unicode('Hello, AndrĆ©', self.gpg.encoding) - data = data.encode(self.gpg.encoding) - sig = self.gpg.sign(data, keyid=key.fingerprint, passphrase='bbrown') + def test_signature_string(self): + """Test that signing a message string works.""" + key = self.generate_key("Werner Koch", "gnupg.org") + message = "Damn, I really wish GnuPG had ECC support." + sig = self.gpg.sign(message, keyid=key.fingerprint, + passphrase='wernerkoch') + print "SIGNATURE:\n", sig.data + self.assertIsNotNone(sig.data) + + def test_signature_algorithm(self): + """Test that determining the signing algorithm works.""" + key = self.generate_key("Werner Koch", "gnupg.org") + message = "Damn, I really wish GnuPG had ECC support." + sig = self.gpg.sign(message, keyid=key.fingerprint, + passphrase='wernerkoch') + print "ALGORITHM:\n", sig.sig_algo + self.assertIsNotNone(sig.sig_algo) + + def test_signature_string_bad_passphrase(self): + """Test that signing and verification works.""" + key = self.generate_key("Ron Rivest", "rsa.com") + message = 'Hello, AndrĆ©!' + sig = self.gpg.sign(message, keyid=key.fingerprint, passphrase='foo') self.assertFalse(sig, "Bad passphrase should fail") - sig = self.gpg.sign(data, keyid=key.fingerprint, passphrase='aable') + + def test_signature_string_alternate_encoding(self): + key = self.generate_key("Adi Shamir", "rsa.com") + self.gpg.encoding = 'latin-1' + message = 'Hello, AndrĆ©!' + sig = self.gpg.sign(message, keyid=key.fingerprint, + passphrase='adishamir') + self.assertTrue(sig) + + def test_signature_file(self): + """Test that signing a message file works.""" + key = self.generate_key("Leonard Adleman", "rsa.com") + message = "Someone should add GCM block cipher mode to PyCrypto." + message_fn = os.path.join(tempfile.gettempdir(), 'test_signature_file') + with open(message_fn, 'w+b') as msg: + msg.write(message) + + message_file = buffer(open(message_fn, "rb").read()) + mf = io.BytesIO(message_file) + + sig = self.gpg.sign(mf, keyid=key.fingerprint, + passphrase='leonardadleman') + self.assertTrue(sig, "Good passphrase should succeed") + + def test_signature_string_verification(self): + """Test verification of a signature from a message string.""" + key = self.generate_key("Andrew Able", "alpha.com") + message = 'Hello, AndrĆ©!' + sig = self.gpg.sign(message, keyid=key.fingerprint, + passphrase='andrewable') self.assertTrue(sig, "Good passphrase should succeed") verified = self.gpg.verify(sig.data) + self.assertIsNotNone(verified.fingerprint) if key.fingerprint != verified.fingerprint: logger.debug("key: %r", key.fingerprint) logger.debug("ver: %r", verified.fingerprint) @@ -528,15 +557,14 @@ class GPGTestCase(unittest.TestCase): "Fingerprints must match") self.assertEqual(verified.trust_level, verified.TRUST_ULTIMATE) self.assertEqual(verified.trust_text, 'TRUST_ULTIMATE') - if not os.path.exists('random_binary_data'): - data_file = open('random_binary_data', 'wb') - data_file.write(os.urandom(5120 * 1024)) - data_file.close() - data_file = open('random_binary_data', 'rb') - sig = self.gpg.sign_file(data_file, keyid=key.fingerprint, - passphrase='aable') - data_file.close() - self.assertTrue(sig, "File signing should succeed") + + def test_signature_file_verification(self): + """Test verfication of a signature on a message file.""" + key = self.generate_key("Taher ElGamal", "cryto.me") + message = 'Ų£ŲµŲ­Ų§ŲØ المصالح لا ŁŠŲ­ŲØŁˆŁ† Ų§Ł„Ų«ŁˆŲ±Ų§ŲŖŲ²' + sig = self.gpg.sign(message, keyid=key.fingerprint, + passphrase='taherelgamal') + self.assertTrue(sig, "Good passphrase should succeed") try: file = gnupg._make_binary_stream(sig.data, self.gpg.encoding) verified = self.gpg.verify_file(file) @@ -548,8 +576,8 @@ class GPGTestCase(unittest.TestCase): self.assertEqual(key.fingerprint, verified.fingerprint, "Fingerprints must match") data_file = open('random_binary_data', 'rb') - sig = self.gpg.sign_file(data_file, keyid=key.fingerprint, - passphrase='aable', detach=True) + sig = self.gpg._sign_file(data_file, keyid=key.fingerprint, + passphrase='andrewable', detach=True) data_file.close() self.assertTrue(sig, "File signing should succeed") try: @@ -565,10 +593,7 @@ class GPGTestCase(unittest.TestCase): logger.debug("test_signature_verification ends") def test_deletion(self): - """ - Test that key deletion works. - """ - logger.debug("test_deletion begins") + """Test that key deletion works.""" self.gpg.import_keys(KEYS_TO_IMPORT) public_keys = self.gpg.list_keys() self.assertTrue(is_list_with_len(public_keys, 2), @@ -580,11 +605,7 @@ class GPGTestCase(unittest.TestCase): logger.debug("test_deletion ends") def test_file_encryption_and_decryption(self): - """ - Test that encryption/decryption to/from file works. - """ - logger.debug("test_file_encryption_and_decryption begins") - + """Test that encryption/decryption to/from file works.""" encfname = _make_tempfile() logger.debug('Created tempfile for encrypted content: %s' % encfname) decfname = _make_tempfile() @@ -593,10 +614,10 @@ class GPGTestCase(unittest.TestCase): #os.close(encfno) #os.close(decfno) try: - key = self.generate_key("Andrew", "Able", "alpha.com", + key = self.generate_key("Andrew Able", "alpha.com", passphrase="andy") andrew = key.fingerprint - key = self.generate_key("Barbara", "Brown", "beta.com") + key = self.generate_key("Barbara Brown", "beta.com") barbara = key.fingerprint data = "Hello, world!" file = gnupg._make_binary_stream(data, self.gpg.encoding) @@ -617,10 +638,11 @@ class GPGTestCase(unittest.TestCase): logger.debug("test_file_encryption_and_decryption ends") -suites = { 'basic': set(['test_environment', +suites = { 'basic': set(['test_gpghome_creation', 'test_gpg_binary', 'test_gpg_binary_not_abs', 'test_gpg_binary_version_str', + 'test_gpg_binary_not_installed', 'test_list_keys_initial_public', 'test_list_keys_initial_secret', 'test_make_args_drop_protected_options', @@ -634,8 +656,15 @@ suites = { 'basic': set(['test_environment', 'test_dsa_key_generation_with_subkey', 'test_key_generation_with_invalid_key_type', 'test_key_generation_with_empty_value', + 'test_key_generation_override_default_value', 'test_key_generation_with_colons']), - 'sign': set(['test_signature_verification']), + 'sign': set(['test_signature_file_verification', + 'test_signature_file', + 'test_signature_string_bad_passphrase', + 'test_signature_string_alternate_encoding', + 'test_signature_string_verification', + 'test_signature_algorithm', + 'test_signature_string']), 'crypt': set(['test_encryption_and_decryption', 'test_file_encryption_and_decryption']), 'listkeys': set(['test_list_keys_after_generation']), @@ -654,7 +683,6 @@ def _init_logging(): logging.logThreads = True stream_handler = logging.StreamHandler(stream=sys.stdout) stream_handler.setLevel(logging.DEBUG) - logger = gnupg.logger logger.addHandler(stream_handler) logger.debug("Starting the logger...") @@ -696,6 +724,10 @@ def main(args): verbosity=args.verbose, catchbreak=True) + ## Finally, remove our testing directory: + if os.path.isdir(tempfile.gettempdir()): + os.removedirs(tempfile.gettempdir()) + if __name__ == "__main__": suite_names = list() From a1f9d57cf8770ed3c19a376634a8e4e70498cb42 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:30:00 +0000 Subject: [PATCH 143/397] Update setup.py. --- setup.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 7248a0c..03a62f1 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,13 @@ +#!/usr/bin/env python +#-*- coding: utf-8 -*- + from distutils.core import setup -from gnupg.gnupg import __version__ as version -from gnupg.gnupg import __author__ as author +__module__ = 'gnupg' +__version__ = "0.4.0" +__author__ = "Isis Agora Lovecruft" +__contact__ = 'isis@leap.se' +__date__ = "1 April 2013" setup(name = "python-gnupg", description="A wrapper for the Gnu Privacy Guard (GPG or GnuPG)", @@ -9,14 +15,13 @@ setup(name = "python-gnupg", management, encryption and signature functionality from Python programs. \ It is intended for use with Python 2.6 or greater.", license="""Copyright Ā© 2013 Isis Lovecruft, et.al. see LICENSE file.""", - version=version, - author=author, - author_email="isis@leap.se", - maintainer="Isis Agora Lovecruft", - maintainer_email="isis@leap.se", + version=__version__, + author=__author__, + author_email=__contact__, + maintainer=__author__, + maintainer_email=__contact__, url="https://github.com/isislovecruft/python-gnupg", - packages_dir={'': 'gnupg'}, - packages=[''], + packages=['gnupg', 'gnupg.tests'], platforms="Linux, BSD, OSX, Windows", download_url="https://github.com/isislovecruft/python-gnupg/archive/develop.zip", classifiers=[ From 07310a860e006e34fc23651bfad038facd87ccc8 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 15 Apr 2013 01:42:55 +0000 Subject: [PATCH 144/397] Move _make_binary_stream() to util module and update unittests. --- gnupg/gnupg.py | 38 +++++++++++--------------------------- gnupg/tests/test_gnupg.py | 3 ++- gnupg/util.py | 17 +++++++++++++++++ 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 6d384e9..4bb417e 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -158,20 +158,6 @@ def _copy_data(instream, outstream): else: logger.debug("closed output, %d bytes sent", sent) -def _make_binary_stream(s, encoding): - try: - if util._py3k: - if isinstance(s, str): - s = s.encode(encoding) - else: - if type(s) is not str: - s = s.encode(encoding) - from io import BytesIO - rv = BytesIO(s) - except ImportError: - rv = StringIO(s) - return rv - def _threaded_copy_data(instream, outstream): """Copy data from one stream to another in a separate thread. @@ -431,7 +417,7 @@ class GPG(object): if not util._py3k: message = unicode(message, self.encoding) message = message.encode(self.encoding) - f = _make_binary_stream(message, self.encoding) + f = util._make_binary_stream(message, self.encoding) result = self._sign_file(f, **kwargs) f.close() else: @@ -492,7 +478,7 @@ class GPG(object): >>> assert verify """ - f = _make_binary_stream(data, self.encoding) + f = util._make_binary_stream(data, self.encoding) result = self.verify_file(f) f.close() return result @@ -596,7 +582,7 @@ class GPG(object): result = self._result_map['import'](self) logger.debug('import_keys: %r', key_data[:256]) - data = _make_binary_stream(key_data, self.encoding) + data = util._make_binary_stream(key_data, self.encoding) self._handle_io(['--import'], data, result, binary=True) logger.debug('import_keys result: %r', result.__dict__) data.close() @@ -615,7 +601,7 @@ class GPG(object): safe_keyserver = _fix_unsafe(keyserver) result = self._result_map['import'](self) - data = _make_binary_stream("", self.encoding) + data = util._make_binary_stream("", self.encoding) args = ['--keyserver', keyserver, '--recv-keys'] if keyids: @@ -721,7 +707,7 @@ class GPG(object): """ args = ["--gen-key --batch"] key = self._result_map['generate'](self) - f = _make_binary_stream(input, self.encoding) + f = util._make_binary_stream(input, self.encoding) self._handle_io(args, f, key, binary=True) f.close() return key @@ -879,7 +865,7 @@ class GPG(object): >>> assert result.fingerprint == print1 """ - data = _make_binary_stream(data, self.encoding) + data = util._make_binary_stream(data, self.encoding) result = self.encrypt_file(data, recipients, **kwargs) data.close() return result @@ -890,7 +876,7 @@ class GPG(object): :param message: A string or file-like object to decrypt. """ - data = _make_binary_stream(message, self.encoding) + data = util._make_binary_stream(message, self.encoding) result = self.decrypt_file(data, **kwargs) data.close() return result @@ -986,7 +972,7 @@ class GPGWrapper(GPG): """ result = self._result_map['list'](self) gnupg.logger.debug('send_keys: %r', keyids) - data = gnupg._make_binary_stream("", self.encoding) + data = gnupg.util._make_binary_stream("", self.encoding) args = ['--keyserver', keyserver, '--send-keys'] args.extend(keyids) self._handle_io(args, data, result, binary=True) @@ -1028,11 +1014,9 @@ class GPGWrapper(GPG): def list_packets(self, raw_data): args = ["--list-packets"] result = self._result_map['list-packets'](self) - self._handle_io( - args, - _make_binary_stream(raw_data, self.encoding), - result, - ) + self._handle_io(args, + util._make_binary_stream(raw_data, self.encoding), + result) return result def encrypted_to(self, raw_data): diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index a3612f6..baba110 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -26,6 +26,7 @@ else: import gnupg from gnupg import parsers +from gnupg import util __author__ = gnupg.__author__ __date__ = gnupg.__date__ @@ -566,7 +567,7 @@ class GPGTestCase(unittest.TestCase): passphrase='taherelgamal') self.assertTrue(sig, "Good passphrase should succeed") try: - file = gnupg._make_binary_stream(sig.data, self.gpg.encoding) + file = util._make_binary_stream(sig.data, self.gpg.encoding) verified = self.gpg.verify_file(file) except UnicodeDecodeError: #happens in Python 2.6 verified = self.gpg.verify_file(io.BytesIO(sig.data)) diff --git a/gnupg/util.py b/gnupg/util.py index 491bcab..d963aca 100644 --- a/gnupg/util.py +++ b/gnupg/util.py @@ -164,6 +164,23 @@ def _is_list_or_tuple(instance): """ return isinstance(instance,list) or isinstance(instance,tuple) +def _make_binary_stream(s, encoding): + """ + xxx fill me in + """ + try: + if _py3k: + if isinstance(s, str): + s = s.encode(encoding) + else: + if type(s) is not str: + s = s.encode(encoding) + from io import BytesIO + rv = BytesIO(s) + except ImportError: + rv = StringIO(s) + return rv + ## xxx unused function? def _today(): """Get the current date. From 807aa9821bfd241f6454f6ad3de83897205fee0d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 16 Apr 2013 22:45:33 +0000 Subject: [PATCH 145/397] Make log statements in _copy_data() more explicit. --- gnupg/gnupg.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 4bb417e..d028507 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -137,26 +137,25 @@ def _copy_data(instream, outstream): if len(data) == 0: break sent += len(data) - logger.debug("sending chunk (%d): %r", sent, data[:256]) + logger.debug("_copy_data(): sending chunk (%d):\n%s" % (sent, data[:256])) try: outstream.write(data) except UnicodeError: try: outstream.write(data.encode(enc)) except IOError: - logger.exception('Error sending data: Broken pipe') + logger.exception('_copy_data(): Error sending data: Broken pipe') break except IOError: - # Can sometimes get 'broken pipe' errors even when the - # data has all been sent - logger.exception('Error sending data: Broken pipe') + # Can get 'broken pipe' errors even when all data was sent + logger.exception('_copy_data(): Error sending data: Broken pipe') break try: outstream.close() except IOError: - logger.exception('Got IOError while trying to close FD outstream') + logger.exception('_copy_data(): Got IOError while closing %s' % outstream) else: - logger.debug("closed output, %d bytes sent", sent) + logger.debug("_copy_data(): Closed output, %d bytes sent." % sent) def _threaded_copy_data(instream, outstream): """Copy data from one stream to another in a separate thread. From 5d94acce83fc1d752be86c4973567f17cc9245e4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 16 Apr 2013 23:04:30 +0000 Subject: [PATCH 146/397] Add _is_hex() function to parsers.py for checking keyid/fingerprints. --- gnupg/parsers.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 9bd8a68..78e0355 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -34,6 +34,7 @@ import util ESCAPE_PATTERN = re.compile(r'\\x([0-9a-f][0-9a-f])', re.I) +HEXIDECIMAL = re.compile('([0-9A-F]{2})+') class ProtectedOption(Exception): @@ -327,6 +328,17 @@ def _is_allowed(input): return input return None +def _is_hex(string): + """Check that a string is hexidecimal, with alphabetic characters + capitalized and without whitespace. + + :param str string: The string to check. + """ + matched = HEXIDECIMAL.match(string) + if matched is not None and len(matched.group()) >= 2: + return True + return False + def _sanitise(*args): """Take an arg or the key portion of a kwarg and check that it is in the set of allowed GPG options and flags, and that it has the correct @@ -393,6 +405,12 @@ def _sanitise(*args): else: logger.debug("_check_option(): %s not file: %s" % (flag, val)) + elif flag in ['--default-key']: + if _is_hex(val): + safe_option += (val + " ") + else: + logger.debug("_check_option(): '%s %s' not hex." + % (flag, val)) else: safe_option += (val + " ") logger.debug("_check_option(): No checks for %s" From 70b8269aaf7b72cbb69af8a9318441c29c011496 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 16 Apr 2013 23:06:42 +0000 Subject: [PATCH 147/397] Remove one of the sources of the UnicodeDecodeErrors on Python2.6. --- gnupg/gnupg.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index d028507..49eab2a 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -412,10 +412,6 @@ class GPG(object): if isinstance(message, file): result = self._sign_file(message, **kwargs) elif not util._is_stream(message): - if isinstance(message, str): - if not util._py3k: - message = unicode(message, self.encoding) - message = message.encode(self.encoding) f = util._make_binary_stream(message, self.encoding) result = self._sign_file(f, **kwargs) f.close() From 301fd6f89c487d265bc0cd3c0916e2b3b02f7d2a Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 16 Apr 2013 23:07:37 +0000 Subject: [PATCH 148/397] Clean up the logging statements in _sign_file(). --- gnupg/gnupg.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 49eab2a..3917762 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -424,7 +424,7 @@ class GPG(object): def _sign_file(self, file, keyid=None, passphrase=None, clearsign=True, detach=False, binary=False): """Create a signature for a file.""" - logger.debug("GPG._sign_file(): %s", file) + logger.debug("_sign_file(): %s", file) if binary: args = ['--sign'] else: @@ -433,10 +433,8 @@ class GPG(object): if clearsign: args.append("--clearsign") if detach: - logger.warn( - "Cannot use --clearsign and --detach-sign simultaneously.") - logger.warn( - "Using default GPG behaviour: --clearsign only.") + logger.warn("Cannot use both --clearsign and --detach-sign.") + logger.warn("Using default GPG behaviour: --clearsign only.") elif detach and not clearsign: args.append("--detach-sign") @@ -444,8 +442,8 @@ class GPG(object): args.append(str("--default-key %s" % keyid)) result = self._result_map['sign'](self) - #We could use _handle_io here except for the fact that if the - #passphrase is bad, gpg bails and you can't write the message. + ## We could use _handle_io here except for the fact that if the + ## passphrase is bad, gpg bails and you can't write the message. p = self._open_subprocess(args, passphrase is not None) try: stdin = p.stdin @@ -453,7 +451,7 @@ class GPG(object): _write_passphrase(stdin, passphrase, self.encoding) writer = _threaded_copy_data(file, stdin) except IOError: - logging.exception("error writing message") + logger.exception("_sign_file(): Error writing message") writer = None self._collect_output(p, result, writer, stdin) return result From 2fb88ad6b4abd04239b1544ab775b49e63aab88c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 16 Apr 2013 23:09:05 +0000 Subject: [PATCH 149/397] Fix function verify_file() to handle detached signature files. --- gnupg/gnupg.py | 65 ++++++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 3917762..e2fe3ce 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -476,50 +476,43 @@ class GPG(object): f.close() return result - def verify_file(self, file, data_filename=None): - """ - Verify the signature on the contents of a file or file-like + def verify_file(self, file, sig_file=None): + """Verify the signature on the contents of a file or file-like object. Can handle embedded signatures as well as detached signatures. If using detached signatures, the file containing the - detached signature should be specified as the :param:data_filename. + detached signature should be specified as the ``sig_file``. :param file file: A file descriptor object. Its type will be checked - with :func:util._is_file. - :param file data_filename: A file containing the GPG signature data for - :param:file. If given, :param:file is - verified via this detached signature. + with :func:`util._is_file`. + :param str sig_file: A file containing the GPG signature data for + ``file``. If given, ``file`` is verified via this + detached signature. """ - ## attempt to wrap any escape characters in quotes: - safe_file = _fix_unsafe(file) - ## check that :param:file is actually a file: - util._is_file(safe_file) - - logger.debug('verify_file: %r, %r', safe_file, data_filename) + fn = None result = self._result_map['verify'](self) - args = ['--verify'] - if data_filename is None: - self._handle_io(args, safe_file, result, binary=True) + + if sig_file is None: + logger.debug("verify_file(): Handling embedded signature") + args = ["--verify"] + proc = self._open_subprocess(args) + writer = _threaded_copy_data(file, proc.stdin) + self._collect_output(proc, result, writer, stdin=proc.stdin) else: - safe_data_filename = _fix_unsafe(data_filename) - - logger.debug('Handling detached verification') - fd, fn = tempfile.mkstemp(prefix='pygpg') - - with open(safe_file) as sf: - contents = sf.read() - os.write(fd, s) - os.close(fd) - logger.debug('Wrote to temp file: %r', contents) - args.append(fn) - args.append('"%s"' % safe_data_filename) - - try: - p = self._open_subprocess(args) - self._collect_output(p, result, stdin=p.stdin) - finally: - os.unlink(fn) - + if not util._is_file(sig_file): + logger.debug("verify_file(): '%r' is not a file" % sig_file) + return result + logger.debug('verify_file(): Handling detached verification') + sig_fh = None + try: + sig_fh = open(sig_file) + args = ["--verify %s - " % sig_fh.name] + proc = self._open_subprocess(args) + writer = _threaded_copy_data(file, proc.stdin) + self._collect_output(proc, result, stdin=proc.stdin) + finally: + if sig_fh and not sig_fh.closed: + sig_fh.close() return result # From bada24f8e4784b303380b644fceff770ddc588e4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 16 Apr 2013 23:10:22 +0000 Subject: [PATCH 150/397] Remove resolved TODO comments. --- gnupg/parsers.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 78e0355..c6b8cc8 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -269,12 +269,8 @@ def _is_allowed(input): ## assertion will check that GPG will recognise them ## ## xxx checkout the --store option for creating rfc1991 data packets - ## xxx also --multifile use with verify encrypt & decrypt ## xxx key fetching/retrieving options: [fetch_keys, merge_only, recv_keys] ## - ## xxx which ones do we want as defaults? - ## eg, --no-show-photos would mitigate things like - ## https://www-01.ibm.com/support/docview.wss?uid=swg21620982 _allowed = frozenset( ['--list-keys', '--list-key', '--fixed-list-mode', '--list-secret-keys', '--list-public-keys', From b23812d14ccfe1ac4f4e876a62b9507b007e382e Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 16 Apr 2013 23:10:38 +0000 Subject: [PATCH 151/397] Add option '--no-show-photos' to allowed options. --- gnupg/parsers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index c6b8cc8..2a48e88 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -275,6 +275,7 @@ def _is_allowed(input): ['--list-keys', '--list-key', '--fixed-list-mode', '--list-secret-keys', '--list-public-keys', '--list-packets', '--with-colons', + '--no-show-photos', '--delete-keys', '--delete-secret-keys', '--encrypt', '--encrypt-files', '--decrypt', '--decrypt-files', From 6b6ea0e9d151394a8c4492ae81c21836760eecaf Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 16 Apr 2013 23:10:59 +0000 Subject: [PATCH 152/397] Fix a bug in the options checker which tried to call util._is_file(). --- gnupg/parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 2a48e88..9f2ff15 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -397,7 +397,7 @@ def _sanitise(*args): if flag in ['--encrypt', '--encrypt-files', '--decrypt', '--decrypt-file', '--import', '--verify']: ## Place checks here: - if _is_file(val): + if util._is_file(val): safe_option += (val + " ") else: logger.debug("_check_option(): %s not file: %s" From ac647495333942610151e8a9af13a5bd8bbe9f44 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 16 Apr 2013 23:12:23 +0000 Subject: [PATCH 153/397] Fix several bugs in the options parser, and split the _sanitise() function. --- gnupg/parsers.py | 100 +++++++++++++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 39 deletions(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 9f2ff15..a1b5725 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -414,54 +414,76 @@ def _sanitise(*args): % val) return safe_option - is_flag = lambda x: x.startswith('-') - checked = [] + is_flag = lambda x: x.startswith('--') + + def _make_filo(args_string): + filo = arg.split(' ') + filo.reverse() + logger.debug("_make_filo(): Converted to reverse list: %s" % filo) + return filo + + def _make_groups(filo): + groups = {} + while len(filo) >= 1: + last = filo.pop() + if is_flag(last): + logger.debug("_make_groups(): Got arg: %s" % last) + if last == '--verify': + groups[last] = str(filo.pop()) + ## accept the read-from-stdin arg: + if len(filo) >= 1 and filo[len(filo)-1] == '-': + groups[last] += str(' - \'\'') ## gross hack + else: + groups[last] = str() + while len(filo) > 1 and not is_flag(filo[len(filo)-1]): + logger.debug("_make_groups(): Got value: %s" + % filo[len(filo)-1]) + groups[last] += (filo.pop() + " ") + else: + if len(filo) == 1 and not is_flag(filo[0]): + logger.debug("_make_groups(): Got value: %s" % filo[0]) + groups[last] += filo.pop() + else: + logger.debug("_make_groups(): Got solitary value: %s" % last) + groups["xxx"] = last + return groups + + def _check_groups(groups): + logger.debug("_check_groups(): Got groups: %s" % groups) + checked_groups = [] + for a,v in groups.items(): + v = None if len(v) == 0 else v + safe = _check_option(a, v) + if safe is not None and not safe.strip() == "": + logger.debug("_check_groups(): appending option: %s" % safe) + checked_groups.append(safe) + else: + logger.debug("_check_groups(): dropped option '%s %s'" % (a,v)) + return checked_groups + if args is not None: + option_groups = {} for arg in args: + ## if we're given a string with a bunch of options in it split them + ## up and deal with them separately if isinstance(arg, str): logger.debug("_sanitise(): Got arg string: %s" % arg) - ## if we're given a string with a bunch of options in it split - ## them up and deal with them separately if arg.find(' ') > 0: - filo = arg.split() - filo.reverse() - new_arg, new_value = str(), str() - while len(filo) > 0: - if not is_flag(filo[0]): - logger.debug("_sanitise(): Got non-flag arg %s" - % filo[0]) - new_value += (filo.pop() + " ") - else: - logger.debug("_sanitise(): Got arg: %s" % filo[0]) - new_arg = filo.pop() - if len(filo) > 0: - while not is_flag(filo[0]): - logger.debug("_sanitise(): Got value: %s" - % filo[0]) - new_value += (filo.pop() + " ") - safe = _check_option(new_arg, new_value) - if safe is not None and not safe.strip() == "": - logger.debug("_sanitise(): appending option: %s" - % safe) - checked.append(safe) + filo = _make_filo(arg) + option_groups.update(_make_groups(filo)) else: - safe = _check_option(arg, None) - if safe is not None: - logger.debug("_sanitise(): appending args: %s" % safe) - checked.append(safe) - else: - logger.debug("_sanitise(): got None for safe") + option_groups.update({ arg: "" }) elif isinstance(arg, list): logger.debug("_sanitise(): Got arg list: %s" % arg) - allow = _one_flag(arg) - if allow is not None: - checked.append(allow) + arg.reverse() + option_groups.update(_make_groups(arg)) else: - logger.debug("_sanitise(): got non string or list arg: %s" - % arg) - - sanitised = ' '.join(x for x in checked) - return sanitised + logger.debug("_sanitise(): Got non str or list arg: %s" % arg) + checked = _check_groups(option_groups) + sanitised = ' '.join(x for x in checked) + return sanitised + else: + logger.debug("_sanitise(): Got None for args") def _sanitise_list(arg_list): """A generator for iterating through a list of gpg options and sanitising From 06662b98b07705a9e660d649c565c856f601d603 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 16 Apr 2013 23:13:21 +0000 Subject: [PATCH 154/397] Add class attribute docstrings to Verify(). --- gnupg/parsers.py | 55 +++++++++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index a1b5725..69a14e5 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -511,30 +511,48 @@ class Verify(object): TRUST_FULLY = 3 TRUST_ULTIMATE = 4 - TRUST_LEVELS = { - "TRUST_UNDEFINED" : TRUST_UNDEFINED, - "TRUST_NEVER" : TRUST_NEVER, - "TRUST_MARGINAL" : TRUST_MARGINAL, - "TRUST_FULLY" : TRUST_FULLY, - "TRUST_ULTIMATE" : TRUST_ULTIMATE, - } + TRUST_LEVELS = { "TRUST_UNDEFINED" : TRUST_UNDEFINED, + "TRUST_NEVER" : TRUST_NEVER, + "TRUST_MARGINAL" : TRUST_MARGINAL, + "TRUST_FULLY" : TRUST_FULLY, + "TRUST_ULTIMATE" : TRUST_ULTIMATE, } + + #: True if the signature is valid, False otherwise. + valid = False + #: A string describing the status of the signature verification. + #: Can be one of ``'signature bad'``, ``'signature good'``, + #: ``'signature valid'``, ``'signature error'``, ``'decryption failed'``, + #: ``'no public key'``, ``'key exp'``, or ``'key rev'``. + status = None + #: The fingerprint of the signing keyid. + fingerprint = None + #: The fingerprint of the corresponding public key, which may be different + #: if the signature was created with a subkey. + pubkey_fingerprint = None + #: The keyid of the signing key. + key_id = None + #: xxx I'm not sure how this is different to key_id. + signature_id = None + #: The creation date of the signing key. + creation_date = None + #: The timestamp of the purported signature, if we are unable to parse it. + timestamp = None + #: The userid of the signing key which was used to create the signature. + username = None + #: When the signing key is due to expire. + expire_timestamp = None + #: The timestamp for when the signature was created. + sig_timestamp = None + #: A number 0-4 describing the trust level of the signature. + trust_level = None + #: The string corresponding to the ``trust_level`` number. + trust_text = None def __init__(self, gpg): self.gpg = gpg - self.valid = False - self.fingerprint = self.creation_date = self.timestamp = None - self.signature_id = self.key_id = None - self.username = None - self.status = None - self.pubkey_fingerprint = None - self.expire_timestamp = None - self.sig_timestamp = None - self.trust_text = None - self.trust_level = None def __nonzero__(self): return self.valid - __bool__ = __nonzero__ def handle_status(self, key, value): @@ -558,7 +576,6 @@ class Verify(object): self.creation_date, self.sig_timestamp, self.expire_timestamp) = value.split()[:4] - # may be different if signature is made with a subkey self.pubkey_fingerprint = value.split()[-1] self.status = 'signature valid' elif key == "SIG_ID": From 59b7e9946509a2f52275eb82dc9707d5d0a275bc Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 16 Apr 2013 23:13:52 +0000 Subject: [PATCH 155/397] Fix another bug which was raising UnicodeDecodeErrors due to no encoding found. --- gnupg/parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 69a14e5..e83a03d 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -628,7 +628,7 @@ class Crypt(Verify): __bool__ = __nonzero__ def __str__(self): - return self.data.decode(self.gpg.encoding, self.gpg.decode_errors) + return self.data.decode(self.gpg.encoding, self.gpg._decode_errors) def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. From 1d0853f023ddfc41d9da15bfd5dc3ed0d3e55ff4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 16 Apr 2013 23:44:20 +0000 Subject: [PATCH 156/397] Remove excess whitespace. --- gnupg/parsers.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index e83a03d..ead19e4 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -730,19 +730,14 @@ class Sign(object): #: The type of signature created. sig_type = None - #: The algorithm used to create the signature. sig_algo = None - #: The hash algorithm used to create the signature. sig_hash_also = None - #: The fingerprint of the signing keyid. fingerprint = None - #: The timestamp on the signature. timestamp = None - #: xxx fill me in what = None From f325e735f98d613b81580fa5125aae5343dfd9b1 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 16 Apr 2013 23:44:36 +0000 Subject: [PATCH 157/397] Add a missing underscore to a call to gpg.decode_errors in Sign(). --- gnupg/parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index ead19e4..8d434bb 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -754,7 +754,7 @@ class Sign(object): __bool__ = __nonzero__ def __str__(self): - return self.data.decode(self.gpg.encoding, self.gpg.decode_errors) + return self.data.decode(self.gpg.encoding, self.gpg._decode_errors) def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. From d5985ddd87161594224cbba901fccf4aadab8c80 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 16 Apr 2013 23:52:26 +0000 Subject: [PATCH 158/397] Add check in signature string verfication unittest for signature timestamps. --- gnupg/tests/test_gnupg.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index baba110..00f3cd3 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -17,6 +17,7 @@ import os import shutil import sys import tempfile +import time ## Use unittest2 if we're on Python2.6 or less: if sys.version_info.major == 2 and sys.version_info.minor <= 6: @@ -544,10 +545,12 @@ class GPGTestCase(unittest.TestCase): def test_signature_string_verification(self): """Test verification of a signature from a message string.""" - key = self.generate_key("Andrew Able", "alpha.com") - message = 'Hello, AndrĆ©!' + key = self.generate_key("Bruce Schneier", "schneier.com") + message = '...the government uses the general fear of ' + message += '[hackers in popular culture] to push for more power' sig = self.gpg.sign(message, keyid=key.fingerprint, - passphrase='andrewable') + passphrase='bruceschneier') + now = time.mktime(time.gmtime()) self.assertTrue(sig, "Good passphrase should succeed") verified = self.gpg.verify(sig.data) self.assertIsNotNone(verified.fingerprint) @@ -556,13 +559,12 @@ class GPGTestCase(unittest.TestCase): logger.debug("ver: %r", verified.fingerprint) self.assertEqual(key.fingerprint, verified.fingerprint, "Fingerprints must match") - self.assertEqual(verified.trust_level, verified.TRUST_ULTIMATE) - self.assertEqual(verified.trust_text, 'TRUST_ULTIMATE') + self.assertEqual(verified.status, 'signature valid') + self.assertAlmostEqual(int(now), int(verified.timestamp), delta=1000) + self.assertEqual( + verified.username, + u'Bruce Schneier (python-gnupg tester) ') - def test_signature_file_verification(self): - """Test verfication of a signature on a message file.""" - key = self.generate_key("Taher ElGamal", "cryto.me") - message = 'Ų£ŲµŲ­Ų§ŲØ المصالح لا ŁŠŲ­ŲØŁˆŁ† Ų§Ł„Ų«ŁˆŲ±Ų§ŲŖŲ²' sig = self.gpg.sign(message, keyid=key.fingerprint, passphrase='taherelgamal') self.assertTrue(sig, "Good passphrase should succeed") From 4b45fede16b98ad50e2bab4706a610e15af557ae Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 16 Apr 2013 23:54:23 +0000 Subject: [PATCH 159/397] Add unittests for parsers._fix_unsafe() and parsers._is_hex(). * The _fix_unsafe() unittest proves that the original vulnerability in GPG._open_subprocess(..., shell=True, ...) is handled correctly. --- gnupg/tests/test_gnupg.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 00f3cd3..48b8aa6 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -46,6 +46,8 @@ def _make_tempfile(*args, **kwargs): *args, **kwargs) logger = logging.getLogger('gnupg') +_here = os.path.join(os.path.join(util._repo, 'gnupg'), 'tests') +_files = os.path.join(_here, 'files') KEYS_TO_IMPORT = """-----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1.4.9 (MingW32) @@ -147,6 +149,26 @@ class GPGTestCase(unittest.TestCase): self.pubring = os.path.join(self.homedir, 'pubring.gpg') self.secring = os.path.join(self.homedir, 'secring.gpg') + def test_parsers_fix_unsafe(self): + """Test that unsafe inputs are quoted out and then ignored.""" + shell_input = "\"&coproc /bin/sh\"" + fixed = parsers._fix_unsafe(shell_input) + print fixed + test_file = os.path.join(_files, 'cypherpunk_manifesto') + self.assertTrue(os.path.isfile(test_file)) + has_shell = self.gpg.verify_file(test_file, fixed) + self.assertFalse(has_shell.valid) + + def test_parsers_is_hex_valid(self): + """Test that valid hexidecimal passes the parsers._is_hex() check""" + valid_hex = '0A6A58A14B5946ABDE18E207A3ADB67A2CDB8B35' + self.assertTrue(parsers._is_hex(valid_hex)) + + def test_parsers_is_hex_invalid(self): + """Test that invalid hexidecimal fails the parsers._is_hex() check""" + invalid_hex = 'cipherpunks write code' + self.assertFalse(parsers._is_hex(invalid_hex)) + def test_gpghome_creation(self): """Test the environment by ensuring that setup worked.""" hd = self.homedir @@ -641,7 +663,10 @@ class GPGTestCase(unittest.TestCase): logger.debug("test_file_encryption_and_decryption ends") -suites = { 'basic': set(['test_gpghome_creation', +suites = { 'parsers': set(['test_parsers_fix_unsafe', + 'test_parsers_is_hex_valid', + 'test_parsers_is_hex_invalid',]), + 'basic': set(['test_gpghome_creation', 'test_gpg_binary', 'test_gpg_binary_not_abs', 'test_gpg_binary_version_str', From 7ff7807068d52c217c717f8583400a4d159b1fbf Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 16 Apr 2013 23:58:32 +0000 Subject: [PATCH 160/397] Some trivial string and docstring changes. --- gnupg/tests/test_gnupg.py | 48 +++++++++++++++------------------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 48b8aa6..d932b8f 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -294,51 +294,39 @@ class GPGTestCase(unittest.TestCase): self.assertGreater(key_input.find('Francisco Ferrer'), 0) def test_rsa_key_generation(self): - """ - Test that RSA key generation succeeds. - """ - key = self.generate_key("Barbara Brown", "beta.com") + """Test that RSA key generation succeeds.""" + key = self.generate_key("Ralph Merkle", "xerox.com") self.assertIsNotNone(key.type) self.assertIsNotNone(key.fingerprint) def test_rsa_key_generation_with_unicode(self): - """ - Test that RSA key generation succeeds with unicode characters. - """ + """Test that RSA key generation succeeds with unicode characters.""" key = self.generate_key("AnaĆÆs de Flavigny", "ĆŖtrerien.fr") self.assertIsNotNone(key.type) self.assertIsNotNone(key.fingerprint) def test_rsa_key_generation_with_subkey(self): - """ - Test that RSA key generation succeeds with additional subkey. - """ - key = self.generate_key("Need Caffeine", "nowplea.se", + """Test that RSA key generation succeeds with additional subkey.""" + key = self.generate_key("John Gilmore", "isapu.nk", subkey_type='RSA') self.assertIsNotNone(key.type) self.assertIsNotNone(key.fingerprint) def test_dsa_key_generation(self): - """ - Test that DSA key generation succeeds. - """ - key = self.generate_key("DSA Signonly", "test.com") + """Test that DSA key generation succeeds.""" + key = self.generate_key("Ross Anderson", "bearli.on") self.assertIsNotNone(key.type) self.assertIsNotNone(key.fingerprint) def test_dsa_key_generation_with_unicode(self): - """ - Test that DSA key generation succeeds with unicode characters. - """ + """Test that DSA key generation succeeds with unicode characters.""" key = self.generate_key("ē “å£ŠåˆčØˆć™ć‚‹", "ē “å£ŠåˆčØˆć™ć‚‹.ę—„ęœ¬") self.assertIsNotNone(key.type) self.assertIsNotNone(key.fingerprint) def test_dsa_key_generation_with_subkey(self): - """ - Test that RSA key generation succeeds with additional subkey. - """ - key = self.generate_key("OMG Moar Coffee", "giveitto.me", + """Test that RSA key generation succeeds with additional subkey.""" + key = self.generate_key("Eli Biham", "bearli.on", subkey_type='ELG-E') self.assertIsNotNone(key.type) self.assertIsNotNone(key.fingerprint) @@ -528,26 +516,26 @@ class GPGTestCase(unittest.TestCase): def test_signature_algorithm(self): """Test that determining the signing algorithm works.""" - key = self.generate_key("Werner Koch", "gnupg.org") - message = "Damn, I really wish GnuPG had ECC support." + key = self.generate_key("Ron Rivest", "rsa.com") + message = "Someone should add GCM block cipher mode to PyCrypto." sig = self.gpg.sign(message, keyid=key.fingerprint, - passphrase='wernerkoch') + passphrase='ronrivest') print "ALGORITHM:\n", sig.sig_algo self.assertIsNotNone(sig.sig_algo) def test_signature_string_bad_passphrase(self): """Test that signing and verification works.""" - key = self.generate_key("Ron Rivest", "rsa.com") - message = 'Hello, AndrĆ©!' + key = self.generate_key("Taher ElGamal", "cryto.me") + message = 'Ų£ŲµŲ­Ų§ŲØ المصالح لا ŁŠŲ­ŲØŁˆŁ† Ų§Ł„Ų«ŁˆŲ±Ų§ŲŖŲ²' sig = self.gpg.sign(message, keyid=key.fingerprint, passphrase='foo') self.assertFalse(sig, "Bad passphrase should fail") def test_signature_string_alternate_encoding(self): - key = self.generate_key("Adi Shamir", "rsa.com") + key = self.generate_key("Nos Oignons", "nos-oignons.net") self.gpg.encoding = 'latin-1' - message = 'Hello, AndrĆ©!' + message = "MĆŖle-toi de tes oignons" sig = self.gpg.sign(message, keyid=key.fingerprint, - passphrase='adishamir') + passphrase='nosoignons') self.assertTrue(sig) def test_signature_file(self): From e631272ce62c49e689d7591600e9b6bdd0994eea Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 16 Apr 2013 23:59:29 +0000 Subject: [PATCH 161/397] Fix unittest for checking that signing a file works. --- gnupg/tests/test_gnupg.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index d932b8f..aa4021a 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -541,17 +541,11 @@ class GPGTestCase(unittest.TestCase): def test_signature_file(self): """Test that signing a message file works.""" key = self.generate_key("Leonard Adleman", "rsa.com") - message = "Someone should add GCM block cipher mode to PyCrypto." - message_fn = os.path.join(tempfile.gettempdir(), 'test_signature_file') - with open(message_fn, 'w+b') as msg: - msg.write(message) - - message_file = buffer(open(message_fn, "rb").read()) - mf = io.BytesIO(message_file) - - sig = self.gpg.sign(mf, keyid=key.fingerprint, - passphrase='leonardadleman') - self.assertTrue(sig, "Good passphrase should succeed") + message_file = os.path.join(_files, 'cypherpunk_manifesto') + with open(message_file) as msg: + sig = self.gpg.sign(msg, keyid=key.fingerprint, + passphrase='leonardadleman') + self.assertTrue(sig, "I thought I typed my password correctly...") def test_signature_string_verification(self): """Test verification of a signature from a message string.""" From f369f2e8221260fe6f5692710711271645ff2514 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 00:00:47 +0000 Subject: [PATCH 162/397] Add the cypherpunk manifesto as a lorem ipsum dolor for unittests. --- gnupg/tests/files/cypherpunk_manifesto | 87 ++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 gnupg/tests/files/cypherpunk_manifesto diff --git a/gnupg/tests/files/cypherpunk_manifesto b/gnupg/tests/files/cypherpunk_manifesto new file mode 100644 index 0000000..9e3b503 --- /dev/null +++ b/gnupg/tests/files/cypherpunk_manifesto @@ -0,0 +1,87 @@ +Privacy is necessary for an open society in the electronic age. Privacy is not +secrecy. A private matter is something one doesn't want the whole world to +know, but a secret matter is something one doesn't want anybody to +know. Privacy is the power to selectively reveal oneself to the world. + +If two parties have some sort of dealings, then each has a memory of their +interaction. Each party can speak about their own memory of this; how could +anyone prevent it? One could pass laws against it, but the freedom of speech, +even more than privacy, is fundamental to an open society; we seek not to +restrict any speech at all. If many parties speak together in the same forum, +each can speak to all the others and aggregate together knowledge about +individuals and other parties. The power of electronic communications has +enabled such group speech, and it will not go away merely because we might want +it to. + +Since we desire privacy, we must ensure that each party to a transaction have +knowledge only of that which is directly necessary for that transaction. Since +any information can be spoken of, we must ensure that we reveal as little as +possible. In most cases personal identity is not salient. When I purchase a +magazine at a store and hand cash to the clerk, there is no need to know who I +am. When I ask my electronic mail provider to send and receive messages, my +provider need not know to whom I am speaking or what I am saying or what others +are saying to me; my provider only need know how to get the message there and +how much I owe them in fees. When my identity is revealed by the underlying +mechanism of the transaction, I have no privacy. I cannot here selectively +reveal myself; I must always reveal myself. + +Therefore, privacy in an open society requires anonymous transaction +systems. Until now, cash has been the primary such system. An anonymous +transaction system is not a secret transaction system. An anonymous system +empowers individuals to reveal their identity when desired and only when +desired; this is the essence of privacy. + +Privacy in an open society also requires cryptography. If I say something, I +want it heard only by those for whom I intend it. If the content of my speech +is available to the world, I have no privacy. To encrypt is to indicate the +desire for privacy, and to encrypt with weak cryptography is to indicate not +too much desire for privacy. Furthermore, to reveal one's identity with +assurance when the default is anonymity requires the cryptographic signature. + +We cannot expect governments, corporations, or other large, faceless +organizations to grant us privacy out of their beneficence. It is to their +advantage to speak of us, and we should expect that they will speak. To try to +prevent their speech is to fight against the realities of +information. Information does not just want to be free, it longs to be +free. Information expands to fill the available storage space. Information is +Rumor's younger, stronger cousin; Information is fleeter of foot, has more +eyes, knows more, and understands less than Rumor. + +We must defend our own privacy if we expect to have any. We must come together +and create systems which allow anonymous transactions to take place. People +have been defending their own privacy for centuries with whispers, darkness, +envelopes, closed doors, secret handshakes, and couriers. The technologies of +the past did not allow for strong privacy, but electronic technologies do. + +We the Cypherpunks are dedicated to building anonymous systems. We are +defending our privacy with cryptography, with anonymous mail forwarding +systems, with digital signatures, and with electronic money. + +Cypherpunks write code. We know that someone has to write software to defend +privacy, and since we can't get privacy unless we all do, we're going to write +it. We publish our code so that our fellow Cypherpunks may practice and play +with it. Our code is free for all to use, worldwide. We don't much care if you +don't approve of the software we write. We know that software can't be +destroyed and that a widely dispersed system can't be shut down. + +Cypherpunks deplore regulations on cryptography, for encryption is +fundamentally a private act. The act of encryption, in fact, removes +information from the public realm. Even laws against cryptography reach only so +far as a nation's border and the arm of its violence. Cryptography will +ineluctably spread over the whole globe, and with it the anonymous transactions +systems that it makes possible. + +For privacy to be widespread it must be part of a social contract. People must +come and together deploy these systems for the common good. Privacy only +extends so far as the cooperation of one's fellows in society. We the +Cypherpunks seek your questions and your concerns and hope we may engage you so +that we do not deceive ourselves. We will not, however, be moved out of our +course because some may disagree with our goals. + +The Cypherpunks are actively engaged in making the networks safer for +privacy. Let us proceed together apace. + +Onward. + +Eric Hughes +9 March 1993 From 1a0d27636b95a34f094fdc2d791ecc6b8fbc5b92 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 00:09:21 +0000 Subject: [PATCH 163/397] More string shuffling. --- gnupg/tests/test_gnupg.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index aa4021a..9e64db9 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -569,8 +569,12 @@ class GPGTestCase(unittest.TestCase): verified.username, u'Bruce Schneier (python-gnupg tester) ') + def test_signature_verification_clearsign(self): + """Test verfication of an embedded signature.""" + key = self.generate_key("Johan Borst", "rijnda.el") + message = "You're *still* using AES? Really?" sig = self.gpg.sign(message, keyid=key.fingerprint, - passphrase='taherelgamal') + passphrase='johanborst') self.assertTrue(sig, "Good passphrase should succeed") try: file = util._make_binary_stream(sig.data, self.gpg.encoding) From 0b9ee78d205c424d814008da1f714132cedc7458 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 00:09:45 +0000 Subject: [PATCH 164/397] Add tests for detached signature verification. --- gnupg/tests/test_gnupg.py | 55 +++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 9e64db9..949ebc7 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -586,22 +586,41 @@ class GPGTestCase(unittest.TestCase): logger.debug("ver: %r", verified.fingerprint) self.assertEqual(key.fingerprint, verified.fingerprint, "Fingerprints must match") - data_file = open('random_binary_data', 'rb') - sig = self.gpg._sign_file(data_file, keyid=key.fingerprint, - passphrase='andrewable', detach=True) - data_file.close() - self.assertTrue(sig, "File signing should succeed") - try: - file = gnupg._make_binary_stream(sig.data, self.gpg.encoding) - verified = self.gpg.verify_file(file, 'random_binary_data') - except UnicodeDecodeError: #happens in Python 2.6 - verified = self.gpg.verify_file(io.BytesIO(sig.data)) - if key.fingerprint != verified.fingerprint: - logger.debug("key: %r", key.fingerprint) - logger.debug("ver: %r", verified.fingerprint) - self.assertEqual(key.fingerprint, verified.fingerprint, - "Fingerprints must match") - logger.debug("test_signature_verification ends") + + def test_signature_verification_detached(self): + """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 manifesto: + sig = self.gpg.sign(manifesto, keyid=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(manifesto, sigfilename) + + if key.fingerprint != verified.fingerprint: + logger.debug("key: %r", key.fingerprint) + logger.debug("ver: %r", verified.fingerprint) + + self.assertEqual(key.fingerprint, verified.fingerprint, + "Fingerprints must match") + + def test_signature_verification_detached_binary(self): + """Test that detached signature verification in binary mode fails.""" + key = self.generate_key("Adi Shamir", "rsa.com") + with open(os.path.join(_files, 'cypherpunk_manifesto'), + 'rb') as manifesto: + sig = self.gpg.sign(manifesto, keyid=key.fingerprint, + passphrase='adishamir', + detach=True, binary=True, clearsign=False) + self.assertTrue(sig.data, "File signing should succeed") + with self.assertRaises(UnicodeDecodeError): + print "SIG=", sig def test_deletion(self): """Test that key deletion works.""" @@ -672,7 +691,9 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_key_generation_with_empty_value', 'test_key_generation_override_default_value', 'test_key_generation_with_colons']), - 'sign': set(['test_signature_file_verification', + 'sign': set(['test_signature_verification_clearsign', + 'test_signature_verification_detached', + 'test_signature_verification_detached_binary', 'test_signature_file', 'test_signature_string_bad_passphrase', 'test_signature_string_alternate_encoding', From 8dcf49fa9570f5b495f7c11605f331dada70e3ee Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 00:10:52 +0000 Subject: [PATCH 165/397] Catch OSErrors and IOErrors also in util._is_file if input does not exist. --- gnupg/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/util.py b/gnupg/util.py index d963aca..169d4fd 100644 --- a/gnupg/util.py +++ b/gnupg/util.py @@ -140,7 +140,7 @@ def _is_file(input): """ try: assert os.lstat(input).st_size > 0, "not a file: %s" % input - except (AssertionError, TypeError) as error: + except (AssertionError, TypeError, IOError, OSError) as error: logger.debug(error.message) return False else: From 961b63f9587766d13d0cfad0bcdc9fb54bafde58 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 00:12:18 +0000 Subject: [PATCH 166/397] Add explicit --no-show-photos option to GPG.list_keys() just in case. --- gnupg/gnupg.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index e2fe3ce..a34843a 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -645,13 +645,12 @@ class GPG(object): >>> pubkeys = gpg.list_keys() >>> assert print1 in pubkeys.fingerprints >>> assert print2 in pubkeys.fingerprints - """ which='public-keys' if secret: which='secret-keys' - args = "--list-%s --fixed-list-mode --fingerprint --with-colons" % (which,) + args = "--list-%s --fixed-list-mode --fingerprint --with-colons --no-show-photos" % (which,) args = [args] p = self._open_subprocess(args) From 9dc29470149dec3ed9a81aa4d8a572ba97306595 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 00:14:06 +0000 Subject: [PATCH 167/397] Update docstrings for GPG.decrypt() and GPG.send_keys(). --- gnupg/gnupg.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index a34843a..1d1dfbf 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -856,8 +856,7 @@ class GPG(object): return result def decrypt(self, message, **kwargs): - """ - Decrypt the contents of a string or file-like object :param:message . + """Decrypt the contents of a string or file-like object ``message``. :param message: A string or file-like object to decrypt. """ @@ -952,9 +951,7 @@ class GPGWrapper(GPG): passphrase=passphrase) def send_keys(self, keyserver, *keyids): - """ - Send keys to a keyserver - """ + """Send keys to a keyserver.""" result = self._result_map['list'](self) gnupg.logger.debug('send_keys: %r', keyids) data = gnupg.util._make_binary_stream("", self.encoding) From 56131179c6f9e9e2e6fa4d3830ec8513509dc475 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 00:16:28 +0000 Subject: [PATCH 168/397] Add stub GPG.list_sigs() function. --- gnupg/gnupg.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 1d1dfbf..62b5747 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -677,6 +677,17 @@ class GPG(object): getattr(result, keyword)(L) return result + def list_sigs(self, *keyids): + """xxx implement me + + The GnuPG option '--show-photos', according to the GnuPG manual, "does + not work with --with-colons", but since we can't rely on all versions + of GnuPG to explicitly handle this correctly, we should probably + include it in the args. + """ + ## we will want to include "--no-show-photos" in the args + raise NotImplemented("Functionality for '--list-sigs' not implemented.") + def gen_key(self, input): """ Generate a key; you might use gen_key_input() to create the control From f76fa598276d8f4e81784327ebe4596d9171c8a1 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 00:16:59 +0000 Subject: [PATCH 169/397] Add parsers.py tests to the "make tests" command in the Makefile. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7458256..77dd310 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ cleantest: clean rm gnupg/tests/*.log test: cleantest - python gnupg/tests/test_gnupg.py basic genkey sign + python gnupg/tests/test_gnupg.py parsers basic genkey sign install: python setup.py install --record installed-files.txt From faf102e6ac730be2347d8b79ba4a0cae5694183a Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 20:53:56 +0000 Subject: [PATCH 170/397] Remove old commented out code for keyring generation in GPG.__init__(). --- gnupg/gnupg.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 62b5747..7c9b630 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -242,17 +242,6 @@ class GPG(object): self.secring = os.path.join(self.gpghome, secring) self.pubring = os.path.join(self.gpghome, pubring) - #for ring in [self.secring, self.pubring]: - # if ring and not os.path.isfile(ring): - # with open(ring, 'a+') as ringfile: - # ringfile.write("") - # ringfile.flush() - # try: - # assert util._has_readwrite(ring), \ - # ("Need r+w for %s" % ring) - # except AssertionError as ae: - # logger.debug(ae.message) - self.options = _sanitise(options) if options else None self.encoding = locale.getpreferredencoding() From a9faa376e7b2c1dffeeef36da6222b3f64f7d57d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 20:54:39 +0000 Subject: [PATCH 171/397] Update docstring for GPG.genkey(). --- gnupg/gnupg.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 7c9b630..65c8ee2 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -678,9 +678,8 @@ class GPG(object): raise NotImplemented("Functionality for '--list-sigs' not implemented.") def gen_key(self, input): - """ - Generate a key; you might use gen_key_input() to create the control - input. + """Generate a GnuPG key through batch file key generation. See + :meth:`GPG.gen_key_input()` for creating the control input. >>> gpg = GPG(gpghome="keys") >>> input = gpg.gen_key_input() @@ -689,6 +688,10 @@ class GPG(object): >>> result = gpg.gen_key('foo') >>> assert not result + :param dict input: A dictionary of parameters and values for the new + key. + :returns: The result mapping with details of the new key, which is a + :class:`parsers.GenKey ` object. """ args = ["--gen-key --batch"] key = self._result_map['generate'](self) From 9288f8eead6f27302c8f9218393f1a758cd1ee5c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 20:55:25 +0000 Subject: [PATCH 172/397] Add docstring for GPG.gen_key_input(). --- gnupg/gnupg.py | 75 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 65c8ee2..1b5f57e 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -701,15 +701,64 @@ class GPG(object): return key def gen_key_input(self, **kwargs): - """Generate GnuPG key(s) through batch file key generation. + """Generate a batch file for input to :meth:`GPG.gen_key()`. The GnuPG batch file key generation feature allows unattended key generation by creating a file with special syntax and then providing it - to: gpg --gen-key --batch + to: ``gpg --gen-key --batch``: + + Key-Type: RSA + Key-Length: 4096 + Name-Real: Autogenerated Key + Name-Email: %s@%s + Expire-Date: 2014-04-01 + %pubring foo.gpg + %secring sec.gpg + %commit + + Key-Type: DSA + Key-Length: 1024 + Subkey-Type: ELG-E + Subkey-Length: 1024 + Name-Real: Joe Tester + Name-Comment: with stupid passphrase + Name-Email: joe@foo.bar + Expire-Date: 0 + Passphrase: abc + %pubring foo.pub + %secring foo.sec + %commit see http://www.gnupg.org/documentation/manuals/gnupg-devel/Unattended-GPG-key-generation.html#Unattended-GPG-key-generation for more details. + + >>> gpg = GPG(gpghome="keys") + >>> params = {'name_real':'python-gnupg tester', 'name_email':'test@ing'} + >>> key_input = gpg.gen_key_input(**params) + >>> result = gpg.gen_key(input) + >>> assert result + + :param str name_real: The uid name for the generated key. + :param str name_email: The uid email for the generated key. (default: + $USERNAME@$HOSTNAME) + :param str name_comment: The comment in the uid of the generated key. + :param str key_type: One of 'RSA', 'DSA', or 'ELG-E'. (default: 'RSA') + :param int key_length: The length in bytes of the new key. + (default: 4096) + :param str subkey_type: If ``key_type`` is 'RSA', an additional subkey + can be generated, and it's type must also be 'RSA'. If ``key_type`` + is 'DSA', then the only subkey type which can be generated is + 'ELG-E'. + :param int subkey_length: The length in bytes of the new subkey. + :type expire: int or str + :param expire: If an integer, the number of days before the key will + expire; if 0, the key will not expire. Otherwise, this can be given + as a string in the form w or m or y, i.e. "5m" would mean + that the key will expire in five months, "1w" would expire in one + week, and "3y" would expire in three years. (default: "1y") + :param str passphrase: The passphrase for the new key. """ + parms = {} for key, val in list(kwargs.items()): key = key.replace('_','-').title() @@ -734,28 +783,6 @@ class GPG(object): out += "%commit\n" return out - # Key-Type: RSA - # Key-Length: 1024 - # Name-Real: ISdlink Server on %s - # Name-Comment: Created by %s - # Name-Email: isdlink@%s - # Expire-Date: 0 - # %commit - # - # - # Key-Type: DSA - # Key-Length: 1024 - # Subkey-Type: ELG-E - # Subkey-Length: 1024 - # Name-Real: Joe Tester - # Name-Comment: with stupid passphrase - # Name-Email: joe@foo.bar - # Expire-Date: 0 - # Passphrase: abc - # %pubring foo.pub - # %secring foo.sec - # %commit - # # ENCRYPTION # From 03940342366b0ed02a11899535767cc7c4aaa9d1 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 20:58:49 +0000 Subject: [PATCH 173/397] Add key generation utility functions. * Add util._make_passphrase() for creating a random string passphrase and optionally writing it to a file only readable by the real uid of the running process. * Change the util._today() function to _next_year(), which gives us today's date a year from now, for setting the default expire date in GPG.gen_key_input(). --- gnupg/gnupg.py | 3 ++- gnupg/util.py | 61 +++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 1b5f57e..b130acd 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -767,7 +767,7 @@ class GPG(object): parms.setdefault('Key-Type', 'RSA') parms.setdefault('Key-Length', 4096) parms.setdefault('Name-Real', "Autogenerated Key") - parms.setdefault('Name-Comment', "Generated by python-gnupg") + parms.setdefault('Expire-Date', util._next_year()) try: logname = os.environ['LOGNAME'] except KeyError: @@ -775,6 +775,7 @@ class GPG(object): hostname = socket.gethostname() parms.setdefault('Name-Email', "%s@%s" % (logname.replace(' ', '_'), hostname)) + out = "Key-Type: %s\n" % parms.pop('Key-Type') for key, val in list(parms.items()): out += "%s: %s\n" % (key, val) diff --git a/gnupg/util.py b/gnupg/util.py index 169d4fd..4249559 100644 --- a/gnupg/util.py +++ b/gnupg/util.py @@ -30,6 +30,9 @@ from datetime import datetime import logging import os +import time +import random +import string try: from io import StringIO @@ -181,15 +184,61 @@ def _make_binary_stream(s, encoding): rv = StringIO(s) return rv -## xxx unused function? -def _today(): - """Get the current date. +def _make_passphrase(length=None, save=False, file=None): + """Create a passphrase and write it to a file that only the user can read. + + This is not very secure, and should not be relied upon for actual key + passphrases. + + :param int length: The length in bytes of the string to generate. + + :param file file: The file to save the generated passphrase in. If not + given, defaults to 'passphrase--' in the top-level directory. + """ + if not length: + length = 40 + + passphrase = _make_random_string(length) + + if save: + ruid, euid, suid = os.getresuid() + gid = os.getgid() + now = time.mktime(time.gmtime()) + + if not file: + filename = str('passphrase-%s-%s' % uid, now) + file = os.path.join(_repo, filename) + + with open(file, 'a') as fh: + fh.write(passphrase) + fh.flush() + fh.close() + os.chmod(file, 0600) + os.chown(file, ruid, gid) + + logger.warn("Generated passphrase saved to %s" % file) + return passphrase + +def _make_random_string(length): + """Returns a random lowercase, uppercase, alphanumerical string. + + :param int length: The length in bytes of the string to generate. + """ + chars = string.ascii_lowercase + string.ascii_uppercase + string.digits + return ''.join(random.choice(chars) for x in range(length)) + +def _next_year(): + """Get the date of today plus one year. :rtype: str - :returns: The date, in the format '%Y-%m-%d'. + :returns: The date of this day next year, in the format '%Y-%m-%d'. """ - now_string = datetime.now().__str__() - return now_string.split(' ', 1)[0] + now = datetime.now().__str__() + date = now.split(' ', 1)[0] + year, month, day = date.split('-', 2) + next_year = str(int(year)+1) + return '-'.join((next_year, month, day)) def _which(executable, flags=os.X_OK): """Borrowed from Twisted's :mod:twisted.python.proutils . From c53be427ca4aec8b4ef8e8631bd9d7a5f6a49d03 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 21:02:43 +0000 Subject: [PATCH 174/397] Remove unused code from the unittest runner. --- gnupg/tests/test_gnupg.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 949ebc7..9ad8786 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -11,7 +11,6 @@ import argparse import doctest import logging from functools import wraps -import inspect import io import os import shutil @@ -768,11 +767,6 @@ if __name__ == "__main__": suite_names = list() for name, methodset in suites.items(): suite_names.append(name) - this_file = inspect.getfile(inspect.currentframe()).split('.', 1)[0] - #mod = getattr(this_file, '__dict__', None) - #func = getattr(gnupg.__module__, '__setattr__', None) - #if func is not None: - # func(name, list(methodset)) setattr(GPGTestCase, name, list(methodset)) parser = argparse.ArgumentParser(description="Unittests for python-gnupg") From aba04d5803947202b514e3cf8e4ba0c2150d8467 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 21:03:44 +0000 Subject: [PATCH 175/397] Only create one test directory while running unittests. --- gnupg/tests/test_gnupg.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 9ad8786..0fead7a 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -29,24 +29,24 @@ from gnupg import parsers from gnupg import util __author__ = gnupg.__author__ -__date__ = gnupg.__date__ __version__ = gnupg.__version__ -REPO_DIR = os.getcwd() -HOME_DIR = os.path.join(REPO_DIR, 'keys') -tempfile.tempdir = os.path.join(REPO_DIR, 'tmp_test') +logger = logging.getLogger('gnupg') +_here = os.path.join(os.path.join(util._repo, 'gnupg'), 'tests') +_files = os.path.join(_here, 'files') + +tempfile.tempdir = os.path.join(_here, 'tmp_test') if not os.path.isdir(tempfile.gettempdir()): os.mkdir(tempfile.gettempdir()) +HOME_DIR = tempfile.tempdir + @wraps(tempfile.TemporaryFile) def _make_tempfile(*args, **kwargs): return tempfile.TemporaryFile(dir=tempfile.gettempdir(), *args, **kwargs) -logger = logging.getLogger('gnupg') -_here = os.path.join(os.path.join(util._repo, 'gnupg'), 'tests') -_files = os.path.join(_here, 'files') KEYS_TO_IMPORT = """-----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1.4.9 (MingW32) @@ -215,7 +215,7 @@ class GPGTestCase(unittest.TestCase): cmd = self.gpg._make_args(None, False) expected = ['/usr/bin/gpg', '--status-fd 2 --no-tty', - '--homedir "%s"' % os.path.join(os.getcwd(), 'keys'), + '--homedir "%s"' % HOME_DIR, '--no-default-keyring --keyring %s' % self.pubring, '--secret-keyring %s' % self.secring] self.assertListEqual(cmd, expected) From debe70723b742c13d399e7f4828914901878a3f7 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 21:04:26 +0000 Subject: [PATCH 176/397] We don't have a default Name-Comment in gen_key anymore, so change unittest. --- gnupg/tests/test_gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 0fead7a..14025dc 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -385,9 +385,9 @@ class GPGTestCase(unittest.TestCase): def test_key_generation_with_empty_value(self): """Test that key generation handles empty values.""" - params = {'name_comment': ' '} + params = {'name_real': ' '} batch = self.gpg.gen_key_input(**params) - self.assertTrue('\nName-Comment: Generated by python-gnupg\n' in batch) + self.assertTrue('\nName-Real: Autogenerated Key\n' in batch) def test_key_generation_override_default_value(self): """Test that overriding a default value in gen_key_input() works.""" From b183f6984a84f005696be2f6ab176fa39c395bb0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 22:38:07 +0000 Subject: [PATCH 177/397] Add listkeys tests to Makefile command. --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 77dd310..7c078b4 100644 --- a/Makefile +++ b/Makefile @@ -5,13 +5,13 @@ clean: rm -f ./*.pyo cleantest: clean - mkdir -p gnupg/tests/keys + mkdir -p gnupg/tests/tmp_test touch gnupg/tests/placeholder.log - rm -rf gnupg/tests/keys + rm -rf gnupg/tests/tmp_test rm gnupg/tests/*.log test: cleantest - python gnupg/tests/test_gnupg.py parsers basic genkey sign + python gnupg/tests/test_gnupg.py parsers basic genkey sign listkeys install: python setup.py install --record installed-files.txt From 4e79a29cea02d8b715589420738bb974e8955203 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 22:39:59 +0000 Subject: [PATCH 178/397] Fix the super() calls in ResultStringIO in unittest script. --- gnupg/tests/test_gnupg.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 14025dc..4b9228c 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -117,11 +117,10 @@ def compare_keys(k1, k2): class ResultStringIO(io.StringIO): - def __init__(self): - super(self, io.StringIO).__init__() - + def __init__(self, init_string): + super(ResultStringIO, self).__init__(init_string) def write(self, data): - super(self, io.StringIO).write(unicode(data)) + super(ResultStringIO, self).write(unicode(data)) class GPGTestCase(unittest.TestCase): From 0801492eafdd049b4b86a6c78d9cb48eec4d3ba8 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 22:41:13 +0000 Subject: [PATCH 179/397] Add unittest for _copy_data(). --- gnupg/tests/test_gnupg.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 4b9228c..d64f60c 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -242,16 +242,19 @@ class GPGTestCase(unittest.TestCase): "Empty list expected...got instead: %s" % str(private_keys)) - - def test_copy_data(self): - """ - XXX implement me - XXX add me to a test suite - - Test that _copy_data() is able to duplicate byte streams. - """ - instream = io.BytesIO("This is a string of bytes mapped in memory.") - outstream = str("And this one is just a string.") + def test_copy_data_bytesio(self): + """Test that _copy_data() is able to duplicate byte streams.""" + message = "This is a BytesIO string string in memory." + instream = io.BytesIO(message) + self.assertEqual(unicode(message), instream.getvalue()) + outstream = ResultStringIO(u'result:') + copied = outstream + util._copy_data(instream, outstream) + self.assertTrue(outstream.readable()) + self.assertTrue(outstream.closed) + self.assertFalse(instream.closed) + self.assertTrue(copied.closed) + #self.assertEqual(instream.getvalue()[6:], outstream.getvalue()) def generate_key_input(self, real_name, email_domain, key_length=None, key_type=None, subkey_type=None, passphrase=None): @@ -668,7 +671,8 @@ class GPGTestCase(unittest.TestCase): suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_parsers_is_hex_valid', - 'test_parsers_is_hex_invalid',]), + 'test_parsers_is_hex_invalid', + 'test_copy_data_bytesio',]), 'basic': set(['test_gpghome_creation', 'test_gpg_binary', 'test_gpg_binary_not_abs', From 72b2d9058ebea38759ad923b931684586acbec9e Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 22:42:38 +0000 Subject: [PATCH 180/397] Fix missing method calls in unittest for GPG.list_keys(). --- gnupg/tests/test_gnupg.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index d64f60c..5388734 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -400,8 +400,9 @@ class GPGTestCase(unittest.TestCase): def test_list_keys_after_generation(self): """Test that after key generation, the generated key is available.""" - self.test_list_keys_initial() - self.do_key_generation() + self.test_list_keys_initial_public() + self.test_list_keys_initial_secret() + self.generate_key("Johannes Trithemius", 'iusedcarrierpidgeons@inste.ad') public_keys = self.gpg.list_keys() self.assertTrue(is_list_with_len(public_keys, 1), "1-element list expected") From 0bc97dd48b77d1f9d7744e11c83f2bd776ee98cb Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 22:43:53 +0000 Subject: [PATCH 181/397] Move _copy_data(), _write_passphrase(), and _threaded_copy_data() to util. --- gnupg/gnupg.py | 71 ------------------------------------------------ gnupg/util.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 71 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index b130acd..81927d5 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -110,77 +110,6 @@ from util import logger, _conf import util -def _copy_data(instream, outstream): - """Copy data from one stream to another. - - :type instream: :class:`io.BytesIO` or :class:`io.StringIO` or file - :param instream: A byte stream or open file to read from. - :param file outstream: The file descriptor of a tmpfile to write to. - """ - sent = 0 - - try: - #assert (util._is_stream(instream) - # or isinstance(instream, file)), "instream not stream or file" - assert isinstance(outstream, file), "outstream is not a file" - except AssertionError as ae: - logger.exception(ae) - return - - if hasattr(sys.stdin, 'encoding'): - enc = sys.stdin.encoding - else: - enc = 'ascii' - - while True: - data = instream.read(1024) - if len(data) == 0: - break - sent += len(data) - logger.debug("_copy_data(): sending chunk (%d):\n%s" % (sent, data[:256])) - try: - outstream.write(data) - except UnicodeError: - try: - outstream.write(data.encode(enc)) - except IOError: - logger.exception('_copy_data(): Error sending data: Broken pipe') - break - except IOError: - # Can get 'broken pipe' errors even when all data was sent - logger.exception('_copy_data(): Error sending data: Broken pipe') - break - try: - outstream.close() - except IOError: - logger.exception('_copy_data(): Got IOError while closing %s' % outstream) - else: - logger.debug("_copy_data(): Closed output, %d bytes sent." % sent) - -def _threaded_copy_data(instream, outstream): - """Copy data from one stream to another in a separate thread. - - Wraps ``_copy_data()`` in a :class:`threading.Thread`. - - :type instream: :class:`io.BytesIO` or :class:`io.StringIO` - :param instream: A byte stream to read from. - :param file outstream: The file descriptor of a tmpfile to write to. - """ - copy_thread = threading.Thread(target=_copy_data, - args=(instream, outstream)) - copy_thread.setDaemon(True) - logger.debug('_threaded_copy_data(): %r, %r, %r', copy_thread, - instream, outstream) - copy_thread.start() - return copy_thread - -def _write_passphrase(stream, passphrase, encoding): - passphrase = '%s\n' % passphrase - passphrase = passphrase.encode(encoding) - stream.write(passphrase) - logger.debug("_write_passphrase(): Wrote passphrase.") - - class GPG(object): """Encapsulate access to the gpg executable""" _decode_errors = 'strict' diff --git a/gnupg/util.py b/gnupg/util.py index 4249559..dd9d1f1 100644 --- a/gnupg/util.py +++ b/gnupg/util.py @@ -33,6 +33,8 @@ import os import time import random import string +import sys +import threading try: from io import StringIO @@ -65,6 +67,54 @@ _ugpg = os.path.join(_user, '.gnupg') ## $HOME/.gnupg _conf = os.path.join(os.path.join(_user, '.config'), 'python-gnupg') ## $HOME/.config/python-gnupg + +def _copy_data(instream, outstream): + """Copy data from one stream to another. + + :type instream: :class:`io.BytesIO` or :class:`io.StringIO` or file + :param instream: A byte stream or open file to read from. + :param file outstream: The file descriptor of a tmpfile to write to. + """ + sent = 0 + + #try: + # #assert (util._is_stream(instream) + # # or isinstance(instream, file)), "instream not stream or file" + # assert isinstance(outstream, file), "outstream is not a file" + #except AssertionError as ae: + # logger.exception(ae) + # return + + if hasattr(sys.stdin, 'encoding'): + enc = sys.stdin.encoding + else: + enc = 'ascii' + + while True: + data = instream.read(1024) + if len(data) == 0: + break + sent += len(data) + logger.debug("_copy_data(): sending chunk (%d):\n%s" % (sent, data[:256])) + try: + outstream.write(data) + except UnicodeError: + try: + outstream.write(data.encode(enc)) + except IOError: + logger.exception('_copy_data(): Error sending data: Broken pipe') + break + except IOError: + # Can get 'broken pipe' errors even when all data was sent + logger.exception('_copy_data(): Error sending data: Broken pipe') + break + try: + outstream.close() + except IOError: + logger.exception('_copy_data(): Got IOError while closing %s' % outstream) + else: + logger.debug("_copy_data(): Closed output, %d bytes sent." % sent) + def _create_gpghome(gpghome): """Create the specified GnuPG home directory, if necessary. @@ -240,6 +290,23 @@ def _next_year(): next_year = str(int(year)+1) return '-'.join((next_year, month, day)) +def _threaded_copy_data(instream, outstream): + """Copy data from one stream to another in a separate thread. + + Wraps ``_copy_data()`` in a :class:`threading.Thread`. + + :type instream: :class:`io.BytesIO` or :class:`io.StringIO` + :param instream: A byte stream to read from. + :param file outstream: The file descriptor of a tmpfile to write to. + """ + copy_thread = threading.Thread(target=_copy_data, + args=(instream, outstream)) + copy_thread.setDaemon(True) + logger.debug('_threaded_copy_data(): %r, %r, %r', copy_thread, + instream, outstream) + copy_thread.start() + return copy_thread + def _which(executable, flags=os.X_OK): """Borrowed from Twisted's :mod:twisted.python.proutils . @@ -277,3 +344,9 @@ def _which(executable, flags=os.X_OK): if os.access(pext, flags): result.append(pext) return result + +def _write_passphrase(stream, passphrase, encoding): + passphrase = '%s\n' % passphrase + passphrase = passphrase.encode(encoding) + stream.write(passphrase) + logger.debug("_write_passphrase(): Wrote passphrase.") From bd853d3fdbe2a3710efd9719dc4cd04bb4f6bd0d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 22:48:07 +0000 Subject: [PATCH 182/397] Make the util module private. --- gnupg/gnupg.py | 56 +++++++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 81927d5..fdeb21b 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -107,7 +107,7 @@ from parsers import GenKey, Sign, ListKeys, ListPackets from parsers import _fix_unsafe, _sanitise, _is_allowed, _sanitise_list from util import logger, _conf -import util +import util as _util class GPG(object): @@ -156,12 +156,12 @@ class GPG(object): gpghome = _conf self.gpghome = _fix_unsafe(gpghome) if self.gpghome: - util._create_gpghome(self.gpghome) + _util._create_gpghome(self.gpghome) else: message = ("Unsuitable gpg home dir: %s" % gpghome) logger.debug("GPG.__init__(): %s" % message) - self.gpgbinary = util._find_gpgbinary(gpgbinary) + self.gpgbinary = _util._find_gpgbinary(gpgbinary) if keyring is not None: raise DeprecationWarning("Option 'keyring' changing to 'secring'") @@ -179,7 +179,7 @@ class GPG(object): try: assert self.gpghome is not None, "Got None for self.gpghome" - assert util._has_readwrite(self.gpghome), ("Home dir %s needs r+w" + assert _util._has_readwrite(self.gpghome), ("Home dir %s needs r+w" % self.gpghome) assert self.gpgbinary, "Could not find gpgbinary %s" % full assert isinstance(verbose, bool), "'verbose' must be boolean" @@ -272,7 +272,7 @@ class GPG(object): break logger.debug("chunk: %r" % data[:256]) chunks.append(data) - if util._py3k: + if _util._py3k: # Join using b'' or '', as appropriate result.data = type(data)().join(chunks) else: @@ -317,8 +317,8 @@ class GPG(object): else: stdin = p.stdin if passphrase: - _write_passphrase(stdin, passphrase, self.encoding) - writer = _threaded_copy_data(file, stdin) + _util._write_passphrase(stdin, passphrase, self.encoding) + writer = _util._threaded_copy_data(file, stdin) self._collect_output(p, result, writer, stdin) return result @@ -329,8 +329,8 @@ class GPG(object): """Create a signature for a message or file.""" if isinstance(message, file): result = self._sign_file(message, **kwargs) - elif not util._is_stream(message): - f = util._make_binary_stream(message, self.encoding) + elif not _util._is_stream(message): + f = _util._make_binary_stream(message, self.encoding) result = self._sign_file(f, **kwargs) f.close() else: @@ -366,8 +366,8 @@ class GPG(object): try: stdin = p.stdin if passphrase: - _write_passphrase(stdin, passphrase, self.encoding) - writer = _threaded_copy_data(file, stdin) + _util._write_passphrase(stdin, passphrase, self.encoding) + writer = _util._threaded_copy_data(file, stdin) except IOError: logger.exception("_sign_file(): Error writing message") writer = None @@ -389,7 +389,7 @@ class GPG(object): >>> assert verify """ - f = util._make_binary_stream(data, self.encoding) + f = _util._make_binary_stream(data, self.encoding) result = self.verify_file(f) f.close() return result @@ -401,7 +401,7 @@ class GPG(object): detached signature should be specified as the ``sig_file``. :param file file: A file descriptor object. Its type will be checked - with :func:`util._is_file`. + with :func:`_util._is_file`. :param str sig_file: A file containing the GPG signature data for ``file``. If given, ``file`` is verified via this detached signature. @@ -414,10 +414,10 @@ class GPG(object): logger.debug("verify_file(): Handling embedded signature") args = ["--verify"] proc = self._open_subprocess(args) - writer = _threaded_copy_data(file, proc.stdin) + writer = _util._threaded_copy_data(file, proc.stdin) self._collect_output(proc, result, writer, stdin=proc.stdin) else: - if not util._is_file(sig_file): + if not _util._is_file(sig_file): logger.debug("verify_file(): '%r' is not a file" % sig_file) return result logger.debug('verify_file(): Handling detached verification') @@ -426,7 +426,7 @@ class GPG(object): sig_fh = open(sig_file) args = ["--verify %s - " % sig_fh.name] proc = self._open_subprocess(args) - writer = _threaded_copy_data(file, proc.stdin) + writer = _util._threaded_copy_data(file, proc.stdin) self._collect_output(proc, result, stdin=proc.stdin) finally: if sig_fh and not sig_fh.closed: @@ -486,7 +486,7 @@ class GPG(object): result = self._result_map['import'](self) logger.debug('import_keys: %r', key_data[:256]) - data = util._make_binary_stream(key_data, self.encoding) + data = _util._make_binary_stream(key_data, self.encoding) self._handle_io(['--import'], data, result, binary=True) logger.debug('import_keys result: %r', result.__dict__) data.close() @@ -505,7 +505,7 @@ class GPG(object): safe_keyserver = _fix_unsafe(keyserver) result = self._result_map['import'](self) - data = util._make_binary_stream("", self.encoding) + data = _util._make_binary_stream("", self.encoding) args = ['--keyserver', keyserver, '--recv-keys'] if keyids: @@ -524,7 +524,7 @@ class GPG(object): which='key' if secret: which='secret-key' - if util._is_list_or_tuple(fingerprints): + if _util._is_list_or_tuple(fingerprints): fingerprints = ' '.join(fingerprints) args = ['--batch --delete-%s "%s"' % (which, fingerprints)] result = self._result_map['delete'](self) @@ -537,7 +537,7 @@ class GPG(object): which='' if secret: which='-secret-key' - if util._is_list_or_tuple(keyids): + if _util._is_list_or_tuple(keyids): keyids = ' '.join(['"%s"' % k for k in keyids]) args = ["--armor --export%s %s" % (which, keyids)] p = self._open_subprocess(args) @@ -624,7 +624,7 @@ class GPG(object): """ args = ["--gen-key --batch"] key = self._result_map['generate'](self) - f = util._make_binary_stream(input, self.encoding) + f = _util._make_binary_stream(input, self.encoding) self._handle_io(args, f, key, binary=True) f.close() return key @@ -696,7 +696,7 @@ class GPG(object): parms.setdefault('Key-Type', 'RSA') parms.setdefault('Key-Length', 4096) parms.setdefault('Name-Real', "Autogenerated Key") - parms.setdefault('Expire-Date', util._next_year()) + parms.setdefault('Expire-Date', _util._next_year()) try: logname = os.environ['LOGNAME'] except KeyError: @@ -754,7 +754,7 @@ class GPG(object): args = ['--symmetric'] else: args = ['--encrypt'] - if not util._is_list_or_tuple(recipients): + if not _util._is_list_or_tuple(recipients): recipients = (recipients,) for recipient in recipients: args.append('--recipient "%s"' % recipient) @@ -810,7 +810,7 @@ class GPG(object): >>> assert result.fingerprint == print1 """ - data = util._make_binary_stream(data, self.encoding) + data = _util._make_binary_stream(data, self.encoding) result = self.encrypt_file(data, recipients, **kwargs) data.close() return result @@ -820,7 +820,7 @@ class GPG(object): :param message: A string or file-like object to decrypt. """ - data = util._make_binary_stream(message, self.encoding) + data = _util._make_binary_stream(message, self.encoding) result = self.decrypt_file(data, **kwargs) data.close() return result @@ -914,7 +914,7 @@ class GPGWrapper(GPG): """Send keys to a keyserver.""" result = self._result_map['list'](self) gnupg.logger.debug('send_keys: %r', keyids) - data = gnupg.util._make_binary_stream("", self.encoding) + data = gnupg._util._make_binary_stream("", self.encoding) args = ['--keyserver', keyserver, '--send-keys'] args.extend(keyids) self._handle_io(args, data, result, binary=True) @@ -934,7 +934,7 @@ class GPGWrapper(GPG): args.append('--cipher-algo %s' % cipher_algo) else: args = ['--encrypt'] - if not util._is_list_or_tuple(recipients): + if not _util._is_list_or_tuple(recipients): recipients = (recipients,) for recipient in recipients: args.append('--recipient "%s"' % recipient) @@ -957,7 +957,7 @@ class GPGWrapper(GPG): args = ["--list-packets"] result = self._result_map['list-packets'](self) self._handle_io(args, - util._make_binary_stream(raw_data, self.encoding), + _util._make_binary_stream(raw_data, self.encoding), result) return result From df5346694df2314f9790a85a97bfa09101877ddb Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 17 Apr 2013 22:48:22 +0000 Subject: [PATCH 183/397] Get rid of the "--no-show-photos is deprecated" GnuPG warning. --- gnupg/gnupg.py | 3 ++- gnupg/parsers.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index fdeb21b..a8391df 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -568,7 +568,8 @@ class GPG(object): which='public-keys' if secret: which='secret-keys' - args = "--list-%s --fixed-list-mode --fingerprint --with-colons --no-show-photos" % (which,) + args = "--list-%s --fixed-list-mode --fingerprint " % (which,) + args += "--with-colons --list-options no-show-photos" args = [args] p = self._open_subprocess(args) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 8d434bb..a79ce70 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -275,7 +275,7 @@ def _is_allowed(input): ['--list-keys', '--list-key', '--fixed-list-mode', '--list-secret-keys', '--list-public-keys', '--list-packets', '--with-colons', - '--no-show-photos', + '--list-options', '--delete-keys', '--delete-secret-keys', '--encrypt', '--encrypt-files', '--decrypt', '--decrypt-files', From 85c1cbc0c82329a1fd0b2572985602371b1f3b8c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 21 Apr 2013 08:25:07 +0000 Subject: [PATCH 184/397] Fix #2184, add twisted copyright info for the util._which() function. --- COPYLEFT | 12 ------------ gnupg/__init__.py | 5 ++++- gnupg/copyleft.py | 50 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 13 deletions(-) delete mode 100644 COPYLEFT create mode 100644 gnupg/copyleft.py diff --git a/COPYLEFT b/COPYLEFT deleted file mode 100644 index 6a810bf..0000000 --- a/COPYLEFT +++ /dev/null @@ -1,12 +0,0 @@ - This file is part of python-gnupg, a Python wrapper around GnuPG. - Copyright (C) 2013 Isis Lovecruft - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero 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 - GNU Affero General Public License for more details. diff --git a/gnupg/__init__.py b/gnupg/__init__.py index ed10ed7..726038e 100644 --- a/gnupg/__init__.py +++ b/gnupg/__init__.py @@ -6,11 +6,14 @@ __url__ = 'https://github.com/isislovecruft/python-gnupg' __version__ = '0.4.0' __license__ = 'AGPLv3' +from copyleft import disclaimer as copyright +from copyleft import txcopyright + import gnupg from parsers import Crypt, DeleteResult, ListKeys from parsers import GenKey, Sign, ImportResult, Verify from gnupg import GPG -__all__ = ["gnupg", +__all__ = ["gnupg", "copyright", "Crypt", "DeleteResult", "ListKeys", "GenKey", "Sign", "Encrypt", "ImportResult", "Verify"] diff --git a/gnupg/copyleft.py b/gnupg/copyleft.py new file mode 100644 index 0000000..d557030 --- /dev/null +++ b/gnupg/copyleft.py @@ -0,0 +1,50 @@ +#-*- encoding: utf-8 -*- +''' +Copyright information for python-gnupg. +''' + +copyright = """\ +Copyright (C) 2013 Isis Lovecruft. +See LICENSE for details.""" + +disclaimer = """\ +This file is part of python-gnupg, a Python wrapper around GnuPG. +%s + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero 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 GNU +Affero General Public License for more details.""" % (copyright,) + + +txcopyright = """\ +Where stated, parts of this program were taken from Twisted, which is +licensed as follows: + +Twisted, the Framework of Your Internet +Copyright (c) 2001-2013 Twisted Matrix Laboratories. +See LICENSE for details. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.""" From ce323251ed4c7bd5416ff088abbafc2e0f239f03 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 9 May 2013 19:14:52 +0000 Subject: [PATCH 185/397] Fix a bug due to the _py3k check being moved to the utils module. --- gnupg/tests/test_gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 5388734..96e495b 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -420,7 +420,7 @@ class GPGTestCase(unittest.TestCase): barbara = key.fingerprint gpg = self.gpg gpg.encoding = 'latin-1' - if gnupg._py3k: + if gnupg._util._py3k: data = 'Hello, AndrĆ©!' else: data = unicode('Hello, AndrĆ©', gpg.encoding) From 04a79a2802b53f8fc6eda5774d6edcac9788ac02 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 10 May 2013 09:33:07 +0000 Subject: [PATCH 186/397] Remove the version string from generated keys, encrypted files, and sigs. --- gnupg/gnupg.py | 2 +- gnupg/parsers.py | 2 +- gnupg/tests/test_gnupg.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index a8391df..9771b0c 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -207,7 +207,7 @@ class GPG(object): :func:parsers._sanitise. The ``passphrase`` argument needs to be True if a passphrase will be sent to GPG, else False. """ - cmd = [self.gpgbinary, '--status-fd 2 --no-tty'] + cmd = [self.gpgbinary, '--status-fd 2 --no-tty --no-emit-version'] if self.gpghome: cmd.append('--homedir "%s"' % self.gpghome) if self.pubring: diff --git a/gnupg/parsers.py b/gnupg/parsers.py index a79ce70..95e8438 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -287,7 +287,7 @@ def _is_allowed(input): '--import', '--export', '--export-secret-keys', '--export-secret-subkeys', '--verify', - '--version', '--output', + '--version', '--no-emit-version', '--output', '--status-fd', '--no-tty', '--passphrase-fd', '--homedir', '--no-default-keyring', '--default-key', '--keyring', '--secret-keyring', '--primary-keyring', diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 96e495b..2d93c11 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -49,7 +49,6 @@ def _make_tempfile(*args, **kwargs): KEYS_TO_IMPORT = """-----BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.9 (MingW32) mQGiBEiH4QERBACm48JJsg2XGzWfL7f/fjp3wtrY+JIz6P07s7smr35kve+wl605 nqHtgjnIVpUVsbI9+xhIAPIkFIR6ZcQ7gRDhoT0bWKGkfdQ7YzXedVRPlQLdbpmR @@ -213,7 +212,7 @@ class GPGTestCase(unittest.TestCase): self.gpg.keyring = self.secring cmd = self.gpg._make_args(None, False) expected = ['/usr/bin/gpg', - '--status-fd 2 --no-tty', + '--status-fd 2 --no-tty --no-emit-version', '--homedir "%s"' % HOME_DIR, '--no-default-keyring --keyring %s' % self.pubring, '--secret-keyring %s' % self.secring] From 58300c314932c8835d0abb50c411c95a7e0b6b75 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 10 May 2013 09:34:46 +0000 Subject: [PATCH 187/397] Enable all tests in the Makefile. * To run tests, it is necessary -- due to the module structure -- to do: $ make uninstall && make install && make test which is recommended to do in a virtualenv, or similar such isolated testing environment. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7c078b4..69283ae 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ cleantest: clean rm gnupg/tests/*.log test: cleantest - python gnupg/tests/test_gnupg.py parsers basic genkey sign listkeys + python gnupg/tests/test_gnupg.py parsers basic genkey sign listkeys crypt keyrings import install: python setup.py install --record installed-files.txt From 5d0e45170ff9de9e8b1320d9779083a32b410971 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 10 May 2013 09:39:40 +0000 Subject: [PATCH 188/397] Add a docsting for GPG.delete_keys(). --- gnupg/gnupg.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 9771b0c..8416f95 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -521,9 +521,24 @@ class GPG(object): return result def delete_keys(self, fingerprints, secret=False): + """Delete a key, or list of keys, from the current keyring. + + The keys must be refered to by their full fingerprint for GnuPG to + delete them. If :param:`secret `, the corresponding secret + keyring will be deleted from :attr:`GPG.secring `. + + :type fingerprints: str or list or tuple + :param fingerprints: A string representing the fingerprint (or a + list/tuple of fingerprint strings) for the key(s) + to delete. + + :param bool secret: If True, delete the corresponding secret key(s) + also. (default: False) + """ which='key' if secret: which='secret-key' + if _util._is_list_or_tuple(fingerprints): fingerprints = ' '.join(fingerprints) args = ['--batch --delete-%s "%s"' % (which, fingerprints)] From fdaadcbd2050bf2f5ec421a671623a3e308e5260 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 10 May 2013 09:40:36 +0000 Subject: [PATCH 189/397] Fix a bug due to calling a GnuPG option which was removed several versions ago. --- gnupg/gnupg.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 8416f95..84eec7d 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -535,7 +535,8 @@ class GPG(object): :param bool secret: If True, delete the corresponding secret key(s) also. (default: False) """ - which='key' + + which='keys' if secret: which='secret-key' From 55185d528ba11e6dda296ad9b5f059f5c83944e6 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 10 May 2013 09:42:30 +0000 Subject: [PATCH 190/397] Separate options into tidy groups so that the parser handles them efficiently. --- gnupg/gnupg.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 84eec7d..ed1ab46 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -542,7 +542,10 @@ class GPG(object): if _util._is_list_or_tuple(fingerprints): fingerprints = ' '.join(fingerprints) - args = ['--batch --delete-%s "%s"' % (which, fingerprints)] + + args = ['--batch'] + args.append("--delete-{} {}".format(which, fingerprints)) + result = self._result_map['delete'](self) p = self._open_subprocess(args) self._collect_output(p, result, stdin=p.stdin) From b66fc4ca82e31b099e167440c89bf7e826a59408 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 10 May 2013 09:44:07 +0000 Subject: [PATCH 191/397] Add subkey support and a docstring to GPG.export_keys(). --- gnupg/gnupg.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index ed1ab46..cce96c9 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -551,11 +551,20 @@ class GPG(object): self._collect_output(p, result, stdin=p.stdin) return result - def export_keys(self, keyids, secret=False): - """export the indicated keys. 'keyid' is anything gpg accepts""" + def export_keys(self, keyids, secret=False, subkeys=False): + """Export the indicated ``keyids``. + + :param str keyids: A keyid or fingerprint in any format that GnuPG will + accept. + :param bool secret: If True, export only the secret key. + :param bool subkeys: If True, export the secret subkeys. + """ which='' - if secret: - which='-secret-key' + if subkeys: + which='-secret-subkeys' + elif secret: + which='-secret-keys' + if _util._is_list_or_tuple(keyids): keyids = ' '.join(['"%s"' % k for k in keyids]) args = ["--armor --export%s %s" % (which, keyids)] From d39226510137d6cfcff8f855a84772fae8744d0b Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 10 May 2013 09:45:39 +0000 Subject: [PATCH 192/397] Fix two bugs, one due to an extra pair of quotes, the other due to strformat. --- gnupg/gnupg.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index cce96c9..968475b 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -566,11 +566,14 @@ class GPG(object): which='-secret-keys' if _util._is_list_or_tuple(keyids): - keyids = ' '.join(['"%s"' % k for k in keyids]) - args = ["--armor --export%s %s" % (which, keyids)] + keyids = ' '.join(['%s' % k for k in keyids]) + + args = ["--armor"] + args.append("--export{} {}".format(which, keyids)) + p = self._open_subprocess(args) - # gpg --export produces no status-fd output; stdout will be - # empty in case of failure + ## gpg --export produces no status-fd output; stdout will be empty in + ## case of failure #stdout, stderr = p.communicate() result = self._result_map['delete'](self) # any result will do self._collect_output(p, result, stdin=p.stdin) From f95c3c7c1e34bc533126839706f73f71b0f3dcfb Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 10 May 2013 09:50:56 +0000 Subject: [PATCH 193/397] Remove old TODO comment. --- gnupg/gnupg.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 968475b..a36aad1 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -635,7 +635,6 @@ class GPG(object): of GnuPG to explicitly handle this correctly, we should probably include it in the args. """ - ## we will want to include "--no-show-photos" in the args raise NotImplemented("Functionality for '--list-sigs' not implemented.") def gen_key(self, input): From a7afce039407a9716efe46db7ec97367215fbe5c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 08:43:33 +0000 Subject: [PATCH 194/397] Add a testing mode to GPG.gen_key_input() which uses insecure PRNG. * This should only be used in the unittests, as the PRNG it uses in GnuPG does not create strong keypairs (though it's faster, thus why we're using it for testing). --- gnupg/gnupg.py | 22 +++++++++++++++++++--- gnupg/tests/test_gnupg.py | 5 ++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index a36aad1..b6702d3 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -660,7 +660,7 @@ class GPG(object): f.close() return key - def gen_key_input(self, **kwargs): + def gen_key_input(self, testing=False, **kwargs): """Generate a batch file for input to :meth:`GPG.gen_key()`. The GnuPG batch file key generation feature allows unattended key @@ -720,27 +720,43 @@ class GPG(object): """ parms = {} + for key, val in list(kwargs.items()): key = key.replace('_','-').title() if str(val).strip(): # skip empty strings parms[key] = val + parms.setdefault('Key-Type', 'RSA') parms.setdefault('Key-Length', 4096) parms.setdefault('Name-Real', "Autogenerated Key") parms.setdefault('Expire-Date', _util._next_year()) + try: logname = os.environ['LOGNAME'] except KeyError: logname = os.environ['USERNAME'] hostname = socket.gethostname() - parms.setdefault('Name-Email', "%s@%s" - % (logname.replace(' ', '_'), hostname)) + + parms.setdefault('Name-Email', "%s@%s" % (logname.replace(' ', '_'), + hostname)) + + if testing: + ## This specific comment string is required by (some? all?) + ## versions of GnuPG to use the insecure PRNG: + parms.setdefault('Name-Comment', 'insecure!') out = "Key-Type: %s\n" % parms.pop('Key-Type') + for key, val in list(parms.items()): out += "%s: %s\n" % (key, val) + out += "%%pubring %s\n" % self.pubring out += "%%secring %s\n" % self.secring + + if testing: + out += "%no-protection\n" + out += "%transient-key\n" + out += "%commit\n" return out diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 2d93c11..bc85176 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -265,7 +265,6 @@ class GPGTestCase(unittest.TestCase): batch = {'Key-Type': key_type, 'Key-Length': key_length, - 'Name-Comment': 'python-gnupg tester', 'Expire-Date': 1, 'Name-Real': '%s' % real_name, 'Name-Email': ("%s@%s" % (name, email_domain))} @@ -276,7 +275,7 @@ class GPGTestCase(unittest.TestCase): batch['Subkey-Type'] = subkey_type batch['Subkey-Length'] = key_length - key_input = self.gpg.gen_key_input(**batch) + key_input = self.gpg.gen_key_input(testing=True, **batch) return key_input def generate_key(self, real_name, email_domain, **kwargs): @@ -568,7 +567,7 @@ class GPGTestCase(unittest.TestCase): self.assertAlmostEqual(int(now), int(verified.timestamp), delta=1000) self.assertEqual( verified.username, - u'Bruce Schneier (python-gnupg tester) ') + u'Bruce Schneier (insecure!) ') def test_signature_verification_clearsign(self): """Test verfication of an embedded signature.""" From b562a346ff1eb093d404d90dff539fbffe9bb64c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 08:48:12 +0000 Subject: [PATCH 195/397] Fix typo in docstring. --- gnupg/parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 95e8438..b42d388 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -697,7 +697,7 @@ class GenKey(object): raise ValueError("Unknown status message: %r" % key) class DeleteResult(object): - """Handle status messages for --delete-key and --delete-secret-key""" + """Handle status messages for --delete-keys and --delete-secret-keys""" def __init__(self, gpg): self.gpg = gpg self.status = 'ok' From 863ad194a58b608bff79b2cd632933f876ffba3d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 08:48:39 +0000 Subject: [PATCH 196/397] Cleanup indentation style a bit. --- gnupg/parsers.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index b42d388..0148a04 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -705,11 +705,9 @@ class DeleteResult(object): def __str__(self): return self.status - problem_reason = { - '1': 'No such key', - '2': 'Must delete secret key first', - '3': 'Ambigious specification', - } + problem_reason = { '1': 'No such key', + '2': 'Must delete secret key first', + '3': 'Ambigious specification', } def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. From 228cec9533f107d0723b0185f39899671737c010 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 08:49:37 +0000 Subject: [PATCH 197/397] Add all available key fields for GPG.gen_key_input to the docstring. --- gnupg/gnupg.py | 144 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 123 insertions(+), 21 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index b6702d3..377a892 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -689,34 +689,136 @@ class GPG(object): %secring foo.sec %commit - see http://www.gnupg.org/documentation/manuals/gnupg-devel/Unattended-GPG-key-generation.html#Unattended-GPG-key-generation - for more details. - >>> gpg = GPG(gpghome="keys") >>> params = {'name_real':'python-gnupg tester', 'name_email':'test@ing'} >>> key_input = gpg.gen_key_input(**params) >>> result = gpg.gen_key(input) >>> assert result - :param str name_real: The uid name for the generated key. - :param str name_email: The uid email for the generated key. (default: - $USERNAME@$HOSTNAME) - :param str name_comment: The comment in the uid of the generated key. + :param bool testing: Uses a faster, albeit insecure random number + generator to create keys. This should only be used + for testing purposes, for keys which are going to + be created and then soon after destroyed, and + never for the generation of actual use keys. + + :param str name_real: The name portion of 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. + (default: $USER@$(hostname) ) Remember to use + UTF-8 encoding for the entirety of the UID. At + least one of :param:`name_real `, + :param:`name_comment `, or + :param:`name_email ` must be + provided, or else no user ID is created. + :param str key_type: One of 'RSA', 'DSA', or 'ELG-E'. (default: 'RSA') - :param int key_length: The length in bytes of the new key. - (default: 4096) - :param str subkey_type: If ``key_type`` is 'RSA', an additional subkey - can be generated, and it's type must also be 'RSA'. If ``key_type`` - is 'DSA', then the only subkey type which can be generated is - 'ELG-E'. - :param int subkey_length: The length in bytes of the new subkey. - :type expire: int or str - :param expire: If an integer, the number of days before the key will - expire; if 0, the key will not expire. Otherwise, this can be given - as a string in the form w or m or y, i.e. "5m" would mean - that the key will expire in five months, "1w" would expire in one - week, and "3y" would expire in three years. (default: "1y") - :param str passphrase: The passphrase for the new key. + Starts a new parameter block by giving the type of + the primary key. The algorithm must be capable of + signing. This is a required parameter. The + algorithm may either be an OpenPGP algorithm + number or a string with the algorithm name. The + special value ā€˜default’ may be used for algo to + create the default key type; in this case a + :param:`key_usage ` should not be given + and ā€˜default’ must also be used for + :param:`subkey_type `. + + :param int key_length: The requested length of the generated key in + bits. (Default: 4096) + + :param str key_grip: hexstring This is an optional hexidecimal string + which is used to generate a CSR or certificate for + an already existing key. :param:key_length + will be ignored if this parameter is given. + + :param str key_usage: Space or comma delimited string of key + usages. Allowed values are ā€˜encrypt’, ā€˜sign’, and + ā€˜auth’. This is used to generate the key + flags. Please make sure that the algorithm is + capable of this usage. Note that OpenPGP requires + that all primary keys are capable of + certification, so no matter what usage is given + here, the ā€˜cert’ flag will be on. If no + ā€˜Key-Usage’ is specified and the ā€˜Key-Type’ is + not ā€˜default’, all allowed usages for that + particular algorithm are used; if it is not given + but ā€˜default’ is used the usage will be ā€˜sign’. + + :param str subkey_type: This generates a secondary key + (subkey). Currently only one subkey can be + handled. See also ``key_type`` above. + + :param int subkey_length: The length of the secondary subkey in bits. + + :param str subkey_usage: Key usage for a subkey; similar to + ``key_usage``. + + :type expire_date: int or str + :param expire_date: Can be specified as an iso-date or as + [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) or as number of days, + weeks, month or years. The special notation + "seconds=N" is also allowed to directly give an + Epoch value. Without a letter days are + assumed. Note that there is no check done on the + overflow of the type used by OpenPGP for + timestamps. Thus you better make sure that the + given value make sense. Although OpenPGP works with + time intervals, GnuPG uses an absolute value + internally and thus the last year we can represent + is 2105. + + :param str creation_date: Set the creation date of the key as stored in + the key information and which is also part of + the fingerprint calculation. Either a date + like "1986-04-26" or a full timestamp like + "19860426T042640" may be used. The time is + considered to be UTC. If it is not given the + current time is used. + + :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 you to specify a + passphrase for batch key generation -- GnuPG + will ignore the ``passphrase`` parameter, stop, + and ask the user for the new passphrase. + However, we can put the command '%no-protection' + into the batch key generation file to allow a + passwordless key to be created, which can then + have its passphrase set later with '--edit-key'. + + ## TODO add version detection and add the '%no-protection' flag. + + :param str preferences: 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. + + :param str revoker: Should be given as 'algo:fpr' [case sensitive]. + 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.) fpr is the + fingerprint of the designated revoker. The optional + ā€˜sensitive’ flag marks the designated revoker as + sensitive information. Only v4 keys may be + designated revokers. + + :param str keyserver: This is an optional parameter that specifies the + preferred keyserver URL for the key. + + :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 100 characters and should not contain + spaces. It is useful for batch key generation to + associate a key parameter block with a status line. + + :rtype: str + :returns: A suitable input string for the ``GPG.gen_key()`` method, the + latter of which will create the new keypair. + + see http://www.gnupg.org/documentation/manuals/gnupg-devel/Unattended-GPG-key-generation.html#Unattended-GPG-key-generation + for more details. """ parms = {} From ed2bc4d8824f1312a9d336f92f4ce45b71fc6131 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 08:51:48 +0000 Subject: [PATCH 198/397] Split the old test_encryption_and_decryption() up into several smaller tests. --- gnupg/tests/test_gnupg.py | 129 +++++++++++++++++++++++++++----------- 1 file changed, 92 insertions(+), 37 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index bc85176..016c220 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -408,42 +408,6 @@ class GPGTestCase(unittest.TestCase): self.assertTrue(is_list_with_len(private_keys, 1), "1-element list expected") - def test_encryption_and_decryption(self): - """Test that encryption and decryption works.""" - logger.debug("test_encryption_and_decryption begins") - key = self.generate_key("Andrew Able", "alpha.com", - passphrase="andy") - andrew = key.fingerprint - key = self.generate_key("Barbara Brown", "beta.com") - barbara = key.fingerprint - gpg = self.gpg - gpg.encoding = 'latin-1' - if gnupg._util._py3k: - data = 'Hello, AndrĆ©!' - else: - data = unicode('Hello, AndrĆ©', gpg.encoding) - data = data.encode(gpg.encoding) - edata = str(gpg.encrypt(data, barbara)) - self.assertNotEqual(data, edata, "Data must have changed") - ddata = gpg.decrypt(edata, passphrase="bbrown") - if data != ddata.data: - logger.debug("was: %r", data) - logger.debug("new: %r", ddata.data) - self.assertEqual(data, ddata.data, "Round-trip must work") - edata = str(gpg.encrypt(data, [andrew, barbara])) - self.assertNotEqual(data, edata, "Data must have changed") - ddata = gpg.decrypt(edata, passphrase="andy") - self.assertEqual(data, ddata.data, "Round-trip must work") - ddata = gpg.decrypt(edata, passphrase="bbrown") - self.assertEqual(data, ddata.data, "Round-trip must work") - logger.debug("test_encryption_and_decryption ends") - # Test symmetric encryption - data = "chippy was here" - edata = str(gpg.encrypt(data, None, passphrase='bbrown', - symmetric=True)) - ddata = gpg.decrypt(edata, passphrase='bbrown') - self.assertEqual(data, str(ddata)) - def test_public_keyring(self): """Test that the public keyring is found in the gpg home directory.""" self.gpg.keyring = self.pubring @@ -634,6 +598,94 @@ class GPGTestCase(unittest.TestCase): "1-element list expected, got %d" % len(public_keys)) logger.debug("test_deletion ends") + def test_encryption(self): + """Test encryption of a message string.""" + key = self.generate_key("Craig Gentry", "xorr.ox", + passphrase="craiggentry") + gentry = key.fingerprint + key = self.generate_key("Marten van Dijk", "xorr.ox") + dijk = key.fingerprint + gpg = self.gpg + message = ("In 2010 Riggio and Sicari presented a practical application" + " of homomorphic encryption to a hybrid wireless sensor/mesh" + " network. The system enables transparent multi-hop wireless" + " backhauls that are able to perform statistical analysis of" + " different kinds of data (temperature, humidity, etc.) " + "coming from a WSN while ensuring both end-to-end encryption" + "and hop-by-hop authentication.") + encrypted = str(gpg.encrypt(message, dijk)) + self.assertNotEqual(message, encrypted, "Data must have changed") + + def test_encryption_alt_encoding(self): + """Test encryption with latin-1 encoding""" + key = self.generate_key("Craig Gentry", "xorr.ox", + passphrase="craiggentry") + gentry = key.fingerprint + key = self.generate_key("Marten van Dijk", "xorr.ox") + dijk = key.fingerprint + gpg = self.gpg + gpg.encoding = 'latin-1' + if util._py3k: + data = 'Hello, AndrĆ©!' + else: + data = unicode('Hello, AndrĆ©', gpg.encoding) + data = data.encode(gpg.encoding) + encrypted = str(gpg.encrypt(data, gentry)) + self.assertNotEqual(data, encrypted, "Data must have changed") + + def test_encryption_multi_recipient(self): + """Test encrypting a message for multiple recipients""" + key = self.generate_key("Craig Gentry", "xorr.ox", + passphrase="craiggentry") + gentry = key.fingerprint + key = self.generate_key("Marten van Dijk", "xorr.ox") + dijk = key.fingerprint + gpg = self.gpg + message = ("In 2010 Riggio and Sicari presented a practical application" + " of homomorphic encryption to a hybrid wireless sensor/mesh" + " network. The system enables transparent multi-hop wireless" + " backhauls that are able to perform statistical analysis of" + " different kinds of data (temperature, humidity, etc.) " + "coming from a WSN while ensuring both end-to-end encryption" + "and hop-by-hop authentication.") + encrypted2 = str(gpg.encrypt(message, [gentry, dijk])) + self.assertNotEqual(message, encrypted2, "PT and CT should not match") + + def test_decryption(self): + """Test decryption""" + key = self.generate_key("Craig Gentry", "xorr.ox", + passphrase="craiggentry") + gentry = key.fingerprint + key = self.generate_key("Marten van Dijk", "xorr.ox") + dijk = key.fingerprint + gpg = self.gpg + message = ("In 2010 Riggio and Sicari presented a practical application" + " of homomorphic encryption to a hybrid wireless sensor/mesh" + " network. The system enables transparent multi-hop wireless" + " backhauls that are able to perform statistical analysis of" + " different kinds of data (temperature, humidity, etc.) " + "coming from a WSN while ensuring both end-to-end encryption" + "and hop-by-hop authentication.") + encrypted = str(gpg.encrypt(message, dijk)) + decrypted = self.gpg.decrypt(encrypted, passphrase="martenvandijk") + if message != decrypted.data: + logger.debug("was: %r", message) + logger.debug("new: %r", decrypted.data) + self.assertEqual(message, decrypted.data, "Round-trip must work") + + encrypted2 = str(gpg.encrypt(message, [gentry, dijk])) + self.assertNotEqual(message, encrypted2, "PT and CT should not match") + decrypted1 = gpg.decrypt(encrypted2, passphrase="craiggentry") + self.assertEqual(message, decrypted.data, "Round-trip must work") + decrypted2 = gpg.decrypt(encrypted2, passphrase="martenvandijk") + self.assertEqual(message, decrypted.data, "Round-trip must work") + # Test symmetric encryption + data = "chippy was here" + edata = str(gpg.encrypt(data, None, passphrase='bbrown', + symmetric=True)) + decrypted = gpg.decrypt(edata, passphrase='bbrown') + self.assertEqual(data, str(decrypted)) + def test_file_encryption_and_decryption(self): """Test that encryption/decryption to/from file works.""" encfname = _make_tempfile() @@ -701,7 +753,10 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_signature_string_verification', 'test_signature_algorithm', 'test_signature_string']), - 'crypt': set(['test_encryption_and_decryption', + 'crypt': set(['test_encryption', + 'test_encryption_alt_encoding', + 'test_encryption_multi_recipient', + 'test_decryption', 'test_file_encryption_and_decryption']), 'listkeys': set(['test_list_keys_after_generation']), 'keyrings': set(['test_public_keyring', From 6c87679e7e776203ce1d9df12e5d13ff0ac77c34 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 08:53:12 +0000 Subject: [PATCH 199/397] Fix a bug in two unittests caused by a method not having been renamed. --- gnupg/tests/test_gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 016c220..7f3084f 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -421,7 +421,7 @@ class GPGTestCase(unittest.TestCase): def test_import_and_export(self): """Test that key import and export works.""" logger.debug("test_import_and_export begins") - self.test_list_keys_initial() + self.test_list_keys_initial_public() gpg = self.gpg result = gpg.import_keys(KEYS_TO_IMPORT) self.assertEqual(result.summary(), '2 imported') @@ -450,7 +450,7 @@ class GPGTestCase(unittest.TestCase): def test_import_only(self): """Test that key import works.""" logger.debug("test_import_only begins") - self.test_list_keys_initial() + self.test_list_keys_initial_public() self.gpg.import_keys(KEYS_TO_IMPORT) public_keys = self.gpg.list_keys() self.assertTrue(is_list_with_len(public_keys, 2), From 79bf77a185cfd11c202331d6460068dec33fa5f9 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 08:54:17 +0000 Subject: [PATCH 200/397] Update test_import_and_export() unittest with newer key generation utility. --- gnupg/tests/test_gnupg.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 7f3084f..c214e2c 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -440,8 +440,9 @@ class GPGTestCase(unittest.TestCase): logger.debug("was: %r", KEYS_TO_IMPORT) logger.debug("now: %r", ascii) self.assertEqual(0, match, "Keys must match") + #Generate a key so we can test exporting private keys - key = self.do_key_generation() + key = self.generate_key('Shai Halevi', 'xorr.ox') ascii = gpg.export_keys(key.fingerprint, True) self.assertTrue(ascii.find("PGP PRIVATE KEY BLOCK") >= 0, "Exported key should be private") From adaf53c2561a99e83502e19e13d8923281a67d29 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 08:55:47 +0000 Subject: [PATCH 201/397] Fix a call in a unittest to _make_binary_stream() to call the correct module. --- gnupg/tests/test_gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index c214e2c..35a2d11 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -703,7 +703,7 @@ class GPGTestCase(unittest.TestCase): key = self.generate_key("Barbara Brown", "beta.com") barbara = key.fingerprint data = "Hello, world!" - file = gnupg._make_binary_stream(data, self.gpg.encoding) + file = util._make_binary_stream(data, self.gpg.encoding) edata = self.gpg.encrypt_file(file, barbara, armor=False, output=encfname) ddata = self.gpg.decrypt_file(efile, passphrase="bbrown", From 458b8a4c2cd7dc0186502536d1af0a1dc2ec3700 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 14:48:45 +0000 Subject: [PATCH 202/397] Remove unused logger statement. --- gnupg/gnupg.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 377a892..6e50d18 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -150,8 +150,6 @@ class GPG(object): problem invoking gpg. """ - logger.warn("") - if not gpghome: gpghome = _conf self.gpghome = _fix_unsafe(gpghome) From 7779a87fac5db89b63f70e729055f58b0d853608 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 17:33:09 +0000 Subject: [PATCH 203/397] Ignore socket configuration files for emacs. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5abff24..f9987a1 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ nosetests.xml .pydevproject # Temp files +.\#* \#*\# *~ From e4f2d533b1a313a2693d0d11405235ec4a82dc24 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 17:48:04 +0000 Subject: [PATCH 204/397] Add cipher, compress, and digest preferences and rename parameters. * Rename gpghome to homedir. * Rename gpgbinary to binary. * Add setting to append '--no-use-agent' to the command string in an attempt to overcome bugs resulting on systems where the user has gpg-agent running in the background (with some configurations, this is run before X is started, and killing the agent can result in X dying) and GnuPG tries to call the program specified by the symlink at /usr/bin/pinentry, result in encryption and decryption failing due to the '--batch' option blasting through pinentry without input. This results in a complaint from GnuPG: gpg: Sorry, no terminal at all requested - can't get input This bug also prevents symmetric encryption/decryption from working in a similar manner. Daniel Kahn Gilmor's monkeysphere, if I am recalling correctly, has a hack to remove the $DISPLAY variable from the users environment, and then add it back in, but frankly this option frightens me, as unsetting the display could result in all X applications failing. Werner Koch's suggestions, from the gnupg-devel mailing list are: http://lists.gnupg.org/pipermail/gnupg-users/2007-April/030927.html And, for the record, '--no-use-agent' doesn't disable pinentry. --- gnupg/gnupg.py | 87 +++++++++++++++++++++------------------ gnupg/tests/test_gnupg.py | 31 +++++++------- gnupg/util.py | 37 +++++++++-------- 3 files changed, 83 insertions(+), 72 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 6e50d18..9716e61 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -122,27 +122,27 @@ class GPG(object): 'sign': Sign, 'verify': Verify,} - def __init__(self, gpgbinary=None, gpghome=None, verbose=False, - use_agent=False, keyring=None, secring=None, pubring=None, - options=None): + def __init__(self, binary=None, homedir=None, verbose=False, + use_agent=False, keyring=None, secring=None, + default_preference_list=None, options=None): """Initialize a GnuPG process wrapper. - :param str gpgbinary: 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 searched for the executable and checked that the real uid/gid of the user has sufficient permissions. - :param str gpghome: Full pathname to directory containing the public + :param str homedir: Full pathname to directory containing the public and private keyrings. Default is whatever GnuPG defaults to. :param str keyring: raises :exc:DeprecationWarning. Use :param:pubring. :param str secring: Name of alternative secret keyring file to use. If left unspecified, this will default to using - 'secring.gpg' in the :param:gpghome directory, and + 'secring.gpg' in the :param:homedir directory, and create that file if it does not exist. :param str pubring: Name of alternative public keyring file to use. If left unspecified, this will default to using - 'pubring.gpg' in the :param:gpghome directory, and + 'pubring.gpg' in the :param:homedir directory, and create that file if it does not exist. :param list options: A list of additional options to pass to the GPG binary. @@ -150,24 +150,28 @@ class GPG(object): problem invoking gpg. """ - if not gpghome: - gpghome = _conf - self.gpghome = _fix_unsafe(gpghome) - if self.gpghome: - _util._create_gpghome(self.gpghome) + if not homedir: + homedir = _conf + self.homedir = _fix_unsafe(homedir) + if self.homedir: + _util._create_homedir(self.homedir) else: - message = ("Unsuitable gpg home dir: %s" % gpghome) + message = ("Unsuitable gpg home dir: %s" % homedir) logger.debug("GPG.__init__(): %s" % message) - self.gpgbinary = _util._find_gpgbinary(gpgbinary) + self.binary = _util._find_binary(binary) - if keyring is not None: - raise DeprecationWarning("Option 'keyring' changing to 'secring'") + if default_preference_list is None: + prefs = 'SHA512 SHA384 SHA256 AES256 CAMELLIA256 TWOFISH ZLIB ZIP' + else: + ## xxx implement me, should return None on error + prefs = check_preference_list(default_preference_list) + self.default_preference_list = prefs secring = 'secring.gpg' if secring is None else _fix_unsafe(secring) - pubring = 'pubring.gpg' if pubring is None else _fix_unsafe(pubring) - self.secring = os.path.join(self.gpghome, secring) - self.pubring = os.path.join(self.gpghome, pubring) + keyring = 'pubring.gpg' if keyring is None else _fix_unsafe(keyring) + self.secring = os.path.join(self.homedir, secring) + self.keyring = os.path.join(self.homedir, keyring) self.options = _sanitise(options) if options else None @@ -176,10 +180,10 @@ class GPG(object): self.encoding = sys.stdin.encoding try: - assert self.gpghome is not None, "Got None for self.gpghome" - assert _util._has_readwrite(self.gpghome), ("Home dir %s needs r+w" - % self.gpghome) - assert self.gpgbinary, "Could not find gpgbinary %s" % full + assert self.homedir is not None, "Got None for self.homedir" + assert _util._has_readwrite(self.homedir), ("Home dir %s needs r+w" + % self.homedir) + assert self.binary, "Could not find binary %s" % full assert isinstance(verbose, bool), "'verbose' must be boolean" assert isinstance(use_agent, bool), "'use_agent' must be boolean" if self.options: @@ -205,17 +209,19 @@ class GPG(object): :func:parsers._sanitise. The ``passphrase`` argument needs to be True if a passphrase will be sent to GPG, else False. """ - cmd = [self.gpgbinary, '--status-fd 2 --no-tty --no-emit-version'] - if self.gpghome: - cmd.append('--homedir "%s"' % self.gpghome) - if self.pubring: - cmd.append('--no-default-keyring --keyring %s' % self.pubring) + cmd = [self.binary, '--status-fd 2 --no-tty --no-emit-version'] + if self.homedir: + cmd.append('--homedir "%s"' % self.homedir) + if self.keyring: + cmd.append('--no-default-keyring --keyring %s' % self.keyring) if self.secring: cmd.append('--secret-keyring %s' % self.secring) if passphrase: cmd.append('--batch --passphrase-fd 0') if self.use_agent: cmd.append('--use-agent') + else: + cmd.append('--no-use-agent') if self.options: [cmd.append(opt) for opt in iter(_sanitise_list(self.options))] if args: @@ -375,7 +381,7 @@ class GPG(object): def verify(self, data): """Verify the signature on the contents of the string ``data``. - >>> gpg = GPG(gpghome="keys") + >>> gpg = GPG(homedir="keys") >>> input = gpg.gen_key_input(Passphrase='foo') >>> key = gpg.gen_key(input) >>> assert key @@ -440,7 +446,7 @@ class GPG(object): >>> import shutil >>> shutil.rmtree("keys") - >>> gpg = GPG(gpghome="keys") + >>> gpg = GPG(homedir="keys") >>> input = gpg.gen_key_input() >>> result = gpg.gen_key(input) >>> print1 = result.fingerprint @@ -495,7 +501,7 @@ class GPG(object): >>> import shutil >>> shutil.rmtree("keys") - >>> gpg = GPG(gpghome="keys") + >>> gpg = GPG(homedir="keys") >>> result = gpg.recv_keys('pgp.mit.edu', '3FF0DB166A7476EA') >>> assert result @@ -583,7 +589,7 @@ class GPG(object): >>> import shutil >>> shutil.rmtree("keys") - >>> gpg = GPG(gpghome="keys") + >>> gpg = GPG(homedir="keys") >>> input = gpg.gen_key_input() >>> result = gpg.gen_key(input) >>> print1 = result.fingerprint @@ -639,7 +645,7 @@ class GPG(object): """Generate a GnuPG key through batch file key generation. See :meth:`GPG.gen_key_input()` for creating the control input. - >>> gpg = GPG(gpghome="keys") + >>> gpg = GPG(homedir="keys") >>> input = gpg.gen_key_input() >>> result = gpg.gen_key(input) >>> assert result @@ -687,7 +693,7 @@ class GPG(object): %secring foo.sec %commit - >>> gpg = GPG(gpghome="keys") + >>> gpg = GPG(homedir="keys") >>> params = {'name_real':'python-gnupg tester', 'name_email':'test@ing'} >>> key_input = gpg.gen_key_input(**params) >>> result = gpg.gen_key(input) @@ -850,7 +856,7 @@ class GPG(object): for key, val in list(parms.items()): out += "%s: %s\n" % (key, val) - out += "%%pubring %s\n" % self.pubring + out += "%%pubring %s\n" % self.keyring out += "%%secring %s\n" % self.secring if testing: @@ -922,7 +928,7 @@ class GPG(object): >>> import shutil >>> if os.path.exists("keys"): ... shutil.rmtree("keys") - >>> gpg = GPG(gpghome="keys") + >>> gpg = GPG(homedir="keys") >>> input = gpg.gen_key_input(passphrase='foo') >>> result = gpg.gen_key(input) >>> print1 = result.fingerprint @@ -1001,10 +1007,10 @@ class GPGWrapper(GPG): replaced by a more general class used throughout the project. """ - def __init__(self, gpgbinary=None, gnupghome=_conf, + def __init__(self, binary=None, homedir=_conf, verbose=False, use_agent=False, keyring=None, options=None): super(GPGWrapper, self).__init__(gnupghome=gnupghome, - gpgbinary=gpgbinary, + binary=binary, verbose=verbose, use_agent=use_agent, keyring=keyring, @@ -1036,13 +1042,14 @@ class GPGWrapper(GPG): raise LookupError( "GnuPG public key for subkey %s not found!" % subkey) - def encrypt(self, data, recipient, sign=None, always_trust=True, + def encrypt(self, data, recipient, sign_with=None, always_trust=True, passphrase=None, symmetric=False): """ Encrypt data using GPG. """ # TODO: devise a way so we don't need to "always trust". - return super(GPGWrapper, self).encrypt(data, recipient, sign=sign, + return super(GPGWrapper, self).encrypt(data, recipient, + sign_with=sign, always_trust=always_trust, passphrase=passphrase, symmetric=symmetric, diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 35a2d11..c3c1c89 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -142,8 +142,8 @@ class GPGTestCase(unittest.TestCase): self.assertTrue(os.path.isdir(hd), "Not a directory: %s" % hd) shutil.rmtree(hd) self.homedir = hd - self.gpg = gnupg.GPG(gpghome=hd, gpgbinary='gpg') - self.pubring = os.path.join(self.homedir, 'pubring.gpg') + self.gpg = gnupg.GPG(homedir=hd, binary='gpg') + self.keyring = os.path.join(self.homedir, 'keyring.gpg') self.secring = os.path.join(self.homedir, 'secring.gpg') def test_parsers_fix_unsafe(self): @@ -198,24 +198,24 @@ class GPGTestCase(unittest.TestCase): env_copy = os.environ path_copy = os.environ.pop('PATH') with self.assertRaises(RuntimeError): - gnupg.GPG(gpghome=self.homedir) + gnupg.GPG(homedir=self.homedir) os.environ = env_copy os.environ.update({'PATH': path_copy}) def test_gpg_binary_not_abs(self): """Test that a non-absolute path to gpg results in a full path.""" - self.assertTrue(os.path.isabs(self.gpg.gpgbinary)) + self.assertTrue(os.path.isabs(self.gpg.binary)) def test_make_args_drop_protected_options(self): """Test that unsupported gpg options are dropped.""" self.gpg.options = ['--tyrannosaurus-rex', '--stegosaurus'] - self.gpg.keyring = self.secring cmd = self.gpg._make_args(None, False) expected = ['/usr/bin/gpg', '--status-fd 2 --no-tty --no-emit-version', - '--homedir "%s"' % HOME_DIR, - '--no-default-keyring --keyring %s' % self.pubring, - '--secret-keyring %s' % self.secring] + '--homedir "%s"' % self.homedir, + '--no-default-keyring --keyring %s' % self.keyring, + '--secret-keyring %s' % self.secring, + '--no-use-agent',] self.assertListEqual(cmd, expected) def test_make_args(self): @@ -223,7 +223,7 @@ class GPGTestCase(unittest.TestCase): not_allowed = ['--bicycle', '--zeppelin', 'train', 'flying-carpet'] self.gpg.options = not_allowed[:-2] args = self.gpg._make_args(not_allowed[2:], False) - self.assertTrue(len(args) == 5) + self.assertTrue(len(args) == 6) for na in not_allowed: self.assertNotIn(na, args) @@ -410,13 +410,15 @@ class GPGTestCase(unittest.TestCase): def test_public_keyring(self): """Test that the public keyring is found in the gpg home directory.""" - self.gpg.keyring = self.pubring - self.assertTrue(os.path.isfile(self.pubring)) + ## we have to use the keyring for GnuPG to create it: + keys = self.gpg.list_keys() + self.assertTrue(os.path.isfile(self.gpg.keyring)) def test_secret_keyring(self): """Test that the secret keyring is found in the gpg home directory.""" - self.gpg.keyring = self.secring - self.assertTrue(os.path.isfile(self.secring)) + ## we have to use the secring for GnuPG to create it: + keys = self.gpg.list_keys(secret=True) + self.assertTrue(os.path.isfile(self.gpg.secring)) def test_import_and_export(self): """Test that key import and export works.""" @@ -725,7 +727,8 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_parsers_is_hex_valid', 'test_parsers_is_hex_invalid', 'test_copy_data_bytesio',]), - 'basic': set(['test_gpghome_creation', + 'basic': set(['test_homedir_creation', + 'test_binary_discovery', 'test_gpg_binary', 'test_gpg_binary_not_abs', 'test_gpg_binary_version_str', diff --git a/gnupg/util.py b/gnupg/util.py index dd9d1f1..a4ddcfe 100644 --- a/gnupg/util.py +++ b/gnupg/util.py @@ -111,28 +111,29 @@ def _copy_data(instream, outstream): try: outstream.close() except IOError: - logger.exception('_copy_data(): Got IOError while closing %s' % outstream) + logger.exception('_copy_data(): Got IOError while closing %s' + % outstream) else: logger.debug("_copy_data(): Closed output, %d bytes sent." % sent) -def _create_gpghome(gpghome): +def _create_homedir(homedir): """Create the specified GnuPG home directory, if necessary. - :param str gpghome: The directory to use. + :param str homedir: The directory to use. :rtype: bool :returns: True if no errors occurred and the directory was created or existed beforehand, False otherwise. """ ## xxx how will this work in a virtualenv? - if not os.path.isabs(gpghome): - message = ("Got non-abs gpg home dir path: %s" % gpghome) - logger.warn("util._create_gpghome(): %s" % message) - gpghome = os.path.abspath(gpghome) - if not os.path.isdir(gpghome): - message = ("Creating gpg home dir: %s" % gpghome) - logger.warn("util._create_gpghome(): %s" % message) + if not os.path.isabs(homedir): + message = ("Got non-abs gpg home dir path: %s" % homedir) + logger.warn("util._create_homedir(): %s" % message) + homedir = os.path.abspath(homedir) + if not os.path.isdir(homedir): + message = ("Creating gpg home dir: %s" % homedir) + logger.warn("util._create_homedir(): %s" % message) try: - os.makedirs(gpghome, 0x1C0) + os.makedirs(homedir, 0x1C0) except OSError as ose: logger.error(ose, exc_info=1) return False @@ -141,22 +142,22 @@ def _create_gpghome(gpghome): else: return True -def _find_gpgbinary(gpgbinary=None): +def _find_binary(binary=None): """Find the absolute path to the GnuPG binary. Also run checks that the binary is not a symlink, and check that our process real uid has exec permissions. - :param str gpgbinary: 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. :rtype: str :returns: The absolute path to the GnuPG binary to use, if no exceptions occur. """ - binary = None - if gpgbinary is not None: - if not os.path.isabs(gpgbinary): - try: binary = _which(gpgbinary)[0] + gpg_binary = None + if binary is not None: + if not os.path.isabs(binary): + try: binary = _which(binary)[0] except IndexError as ie: logger.debug(ie.message) if binary is None: try: binary = _which('gpg')[0] @@ -166,7 +167,7 @@ def _find_gpgbinary(gpgbinary=None): assert not os.path.islink(binary), "Path to gpg binary is symlink" assert os.access(binary, os.X_OK), "Lacking +x perms for gpg binary" except (AssertionError, AttributeError) as ae: - logger.debug("util._find_gpgbinary(): %s" % ae.message) + logger.debug("util._find_binary(): %s" % ae.message) else: return binary From 38057cfe1cec37fe324addd9ed7e3fd75f83c415 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 18:08:41 +0000 Subject: [PATCH 205/397] Add various new options and assign input parsers to them. --- gnupg/parsers.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 0148a04..41fa3d8 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -279,11 +279,17 @@ def _is_allowed(input): '--delete-keys', '--delete-secret-keys', '--encrypt', '--encrypt-files', '--decrypt', '--decrypt-files', + '--always-trust', + '--symmetric', + '--use-agent', '--no-use-agent', '--print-mds', '--print-md', '--sign', '--clearsign', '--detach-sign', '--armor', '--armour', '--gen-key', '--batch', - '--decrypt', '--decrypt-files', + '--decrypt', '--decrypt-files', '--multifile', '--output', + '--cert-digest-algo', '--digest-algo', + '--cipher-algo', '--compress-algo', '--personal-digest-prefs', + '--personal-cipher-prefs', '--personal-compress-prefs', '--import', '--export', '--export-secret-keys', '--export-secret-subkeys', '--verify', @@ -395,14 +401,17 @@ def _sanitise(*args): val = _fix_unsafe(v) if val is not None and val.strip() != "": if flag in ['--encrypt', '--encrypt-files', '--decrypt', - '--decrypt-file', '--import', '--verify']: + '--decrypt-files', '--import', '--verify']: ## Place checks here: if util._is_file(val): safe_option += (val + " ") else: logger.debug("_check_option(): %s not file: %s" % (flag, val)) - elif flag in ['--default-key']: + elif flag in ['--default-key', '--recipient', + '--export', '--export-secret-keys', + '--delete-keys', + '--export-secret-subkeys',]: if _is_hex(val): safe_option += (val + " ") else: From dce4b4a226266040f995e8cbe821f0cbe8b23a99 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 18:10:41 +0000 Subject: [PATCH 206/397] Rename handle_status() parser methods to be private to cleanup the API. --- gnupg/gnupg.py | 2 +- gnupg/parsers.py | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 9716e61..db629be 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -264,7 +264,7 @@ class GPG(object): value = L[1] else: value = "" - result.handle_status(keyword, value) + result._handle_status(keyword, value) result.stderr = ''.join(lines) def _read_data(self, stream, result): diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 41fa3d8..75cb7db 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -564,7 +564,7 @@ class Verify(object): return self.valid __bool__ = __nonzero__ - def handle_status(self, key, value): + def _handle_status(self, key, value): if key in self.TRUST_LEVELS: self.trust_text = key self.trust_level = self.TRUST_LEVELS[key] @@ -639,7 +639,7 @@ class Crypt(Verify): def __str__(self): return self.data.decode(self.gpg.encoding, self.gpg._decode_errors) - def handle_status(self, key, value): + def _handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. @@ -675,7 +675,7 @@ class Crypt(Verify): elif key == "SIGEXPIRED": self.status = 'sig expired' else: - Verify.handle_status(self, key, value) + Verify._handle_status(self, key, value) class GenKey(object): """Handle status messages for --gen-key""" @@ -693,7 +693,7 @@ class GenKey(object): def __str__(self): return self.fingerprint or '' - def handle_status(self, key, value): + def _handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. @@ -718,7 +718,7 @@ class DeleteResult(object): '2': 'Must delete secret key first', '3': 'Ambigious specification', } - def handle_status(self, key, value): + def _handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. @@ -763,7 +763,7 @@ class Sign(object): def __str__(self): return self.data.decode(self.gpg.encoding, self.gpg._decode_errors) - def handle_status(self, key, value): + def _handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. @@ -833,7 +833,7 @@ class ListKeys(list): subkey = [args[4], args[11]] self.curkey['subkeys'].append(subkey) - def handle_status(self, key, value): + def _handle_status(self, key, value): pass @@ -888,7 +888,7 @@ class ImportResult(object): '3': 'Certificate Chain too long', '4': 'Error storing certificate', } - def handle_status(self, key, value): + def _handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. @@ -989,7 +989,7 @@ class Verify(object): return self.valid __bool__ = __nonzero__ - def handle_status(self, key, value): + def _handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. @@ -1064,12 +1064,12 @@ class ListPackets(): self.need_passphrase_sym = None self.userid_hint = None - def handle_status(self, key, value): + def _handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. """ - # TODO: write tests for handle_status + # TODO: write tests for _handle_status if key == 'NODATA': self.nodata = True elif key == 'ENC_TO': From 14ffe145695e2e3e70d0978d947b4b87fcb67309 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 18:16:18 +0000 Subject: [PATCH 207/397] Add a unittest and a fix for hexidecimal inputs with lowercase. --- gnupg/parsers.py | 2 +- gnupg/tests/test_gnupg.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 75cb7db..92566e4 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -34,7 +34,7 @@ import util ESCAPE_PATTERN = re.compile(r'\\x([0-9a-f][0-9a-f])', re.I) -HEXIDECIMAL = re.compile('([0-9A-F]{2})+') +HEXIDECIMAL = re.compile('([0-9A-Fa-f]{2})+') class ProtectedOption(Exception): diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index c3c1c89..0442102 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -161,6 +161,11 @@ class GPGTestCase(unittest.TestCase): valid_hex = '0A6A58A14B5946ABDE18E207A3ADB67A2CDB8B35' self.assertTrue(parsers._is_hex(valid_hex)) + def test_parsers_is_hex_lowercase(self): + """Test parsers._is_hex() with lowercased hexidecimal""" + valid_hex = 'deadbeef15abad1dea' + self.assertTrue(parsers._is_hex(valid_hex)) + def test_parsers_is_hex_invalid(self): """Test that invalid hexidecimal fails the parsers._is_hex() check""" invalid_hex = 'cipherpunks write code' @@ -725,6 +730,7 @@ class GPGTestCase(unittest.TestCase): suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_parsers_is_hex_valid', + 'test_parsers_is_hex_lowercase', 'test_parsers_is_hex_invalid', 'test_copy_data_bytesio',]), 'basic': set(['test_homedir_creation', From d38efaffbc21cf5c5b54e09398c6be7dd4c59955 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 18:17:22 +0000 Subject: [PATCH 208/397] Use proper tempfile isolation of GnuPG homedirs in unittests. --- gnupg/tests/test_gnupg.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 0442102..05418d8 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -35,13 +35,12 @@ __version__ = gnupg.__version__ logger = logging.getLogger('gnupg') _here = os.path.join(os.path.join(util._repo, 'gnupg'), 'tests') _files = os.path.join(_here, 'files') +_tempd = os.path.join(_here, 'tmp') -tempfile.tempdir = os.path.join(_here, 'tmp_test') +tempfile.tempdir = _tempd if not os.path.isdir(tempfile.gettempdir()): os.mkdir(tempfile.gettempdir()) -HOME_DIR = tempfile.tempdir - @wraps(tempfile.TemporaryFile) def _make_tempfile(*args, **kwargs): return tempfile.TemporaryFile(dir=tempfile.gettempdir(), @@ -137,7 +136,7 @@ class GPGTestCase(unittest.TestCase): def setUp(self): """This method is called once per self.test_* method.""" - hd = HOME_DIR + hd = tempfile.mkdtemp() if os.path.exists(hd): self.assertTrue(os.path.isdir(hd), "Not a directory: %s" % hd) shutil.rmtree(hd) @@ -146,6 +145,17 @@ class GPGTestCase(unittest.TestCase): self.keyring = os.path.join(self.homedir, 'keyring.gpg') self.secring = os.path.join(self.homedir, 'secring.gpg') + def tearDown(self): + """This is called once per self.test_* method after the test run.""" + if os.path.exists(self.homedir) and os.path.isdir(self.homedir): + try: + shutil.rmtree(self.homedir) + except OSError as ose: + logger.error(ose) + else: + logger.warn("Can't delete homedir: '%s' not a directory" + % self.homedir) + def test_parsers_fix_unsafe(self): """Test that unsafe inputs are quoted out and then ignored.""" shell_input = "\"&coproc /bin/sh\"" @@ -826,8 +836,8 @@ def main(args): catchbreak=True) ## Finally, remove our testing directory: - if os.path.isdir(tempfile.gettempdir()): - os.removedirs(tempfile.gettempdir()) + if os.path.isdir(_tempd): + os.unlink(_tempd) if __name__ == "__main__": From 2a92e3f10b66314712209ce6da992c8f93f37855 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 18:18:18 +0000 Subject: [PATCH 209/397] Since we use '--no-emit-version', we don't need this anymore. --- gnupg/tests/test_gnupg.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 05418d8..66d2b74 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -109,8 +109,6 @@ def compare_keys(k1, k2): """Compare ASCII keys.""" k1 = k1.split('\n') k2 = k2.split('\n') - del k1[1] # remove version lines - del k2[1] return k1 != k2 From f06c2d73967e43014c1faafc6cd39bc3c43b3e08 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 18:19:04 +0000 Subject: [PATCH 210/397] Cleanup docstring for GPGTestCase.setUpClass(). --- gnupg/tests/test_gnupg.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 66d2b74..8fd30ba 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -124,11 +124,9 @@ class GPGTestCase(unittest.TestCase): @classmethod def setUpClass(cls): - """ - Setup the :class:`GPGTestCase` and runtime environment for tests. + """Setup ``GPGTestCase`` and runtime environment for tests. This function must be called manually. - xxx or is called by TestSuite. """ pass From b76bf502c9227194fd1a204aaa1b1f8d8e9ad6bf Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 18:19:59 +0000 Subject: [PATCH 211/397] Add better unittests for homedir and keyring creation. --- gnupg/tests/test_gnupg.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 8fd30ba..42df0e4 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -177,11 +177,20 @@ class GPGTestCase(unittest.TestCase): invalid_hex = 'cipherpunks write code' self.assertFalse(parsers._is_hex(invalid_hex)) - def test_gpghome_creation(self): - """Test the environment by ensuring that setup worked.""" - hd = self.homedir - self.assertTrue(os.path.exists(hd) and os.path.isdir(hd), - "Not an existing directory: %s" % hd) + def test_homedir_creation(self): + """Test that a homedir is created if left unspecified""" + gpg = gnupg.GPG(binary='gpg') + self.assertTrue(os.path.exists(gpg.homedir), + "Not an existing directory: %s" % gpg.homedir) + self.assertTrue(os.path.isdir(gpg.homedir), + "Not a directory: %s" % gpg.homedir) + + def test_binary_discovery(self): + """Test that the path to gpg is discovered if unspecified""" + gpg = gnupg.GPG() + self.assertIsNotNone(gpg.binary) + self.assertTrue(os.path.exists(gpg.binary), + "Path does not exist: %s" % gpg.binary) def test_gpg_binary(self): """Test that 'gpg --version' does not return an error code.""" From 23b3315c4518a1517542bb55bd75a91d6b64c5ef Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 18:20:49 +0000 Subject: [PATCH 212/397] Remove old logger statements and comments. --- gnupg/tests/test_gnupg.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 42df0e4..90d2383 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -207,7 +207,6 @@ class GPGTestCase(unittest.TestCase): expected2 = "Pubkey:" expected3 = "Cipher:" expected4 = "Compression:" - #logger.debug("'gpg --version' returned output:n%s" % result) self.assertGreater(result.find(expected1), 0) self.assertGreater(result.find(expected2), 0) self.assertGreater(result.find(expected3), 0) @@ -442,7 +441,6 @@ class GPGTestCase(unittest.TestCase): def test_import_and_export(self): """Test that key import and export works.""" - logger.debug("test_import_and_export begins") self.test_list_keys_initial_public() gpg = self.gpg result = gpg.import_keys(KEYS_TO_IMPORT) @@ -468,11 +466,9 @@ class GPGTestCase(unittest.TestCase): ascii = gpg.export_keys(key.fingerprint, True) self.assertTrue(ascii.find("PGP PRIVATE KEY BLOCK") >= 0, "Exported key should be private") - logger.debug("test_import_and_export ends") def test_import_only(self): """Test that key import works.""" - logger.debug("test_import_only begins") self.test_list_keys_initial_public() self.gpg.import_keys(KEYS_TO_IMPORT) public_keys = self.gpg.list_keys() @@ -490,7 +486,6 @@ class GPGTestCase(unittest.TestCase): logger.debug("was: %r", KEYS_TO_IMPORT) logger.debug("now: %r", ascii) self.assertEqual(0, match, "Keys must match") - logger.debug("test_import_only ends") def test_signature_string(self): """Test that signing a message string works.""" @@ -823,7 +818,7 @@ def main(args): tests = unittest.TestSuite(list(map(GPGTestCase, load_tests))) else: tests = prog.testLoader.loadTestsFromTestCase(GPGTestCase) - args.run_doctest = True ## xxx can we set options here? + args.run_doctest = True if args.run_doctest: tests.addTest(doctest.DocTestSuite(gnupg)) logger.debug("Loaded %d tests..." % tests.countTestCases()) From 347cecb9f27f81bba5f74dbb8a574bd583dfb41b Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 18:22:07 +0000 Subject: [PATCH 213/397] Add unittests for multiparty file en/decryption and symmetric en/decryption. --- gnupg/tests/test_gnupg.py | 101 +++++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 40 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 90d2383..715172c 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -624,13 +624,13 @@ class GPGTestCase(unittest.TestCase): key = self.generate_key("Marten van Dijk", "xorr.ox") dijk = key.fingerprint gpg = self.gpg - message = ("In 2010 Riggio and Sicari presented a practical application" - " of homomorphic encryption to a hybrid wireless sensor/mesh" - " network. The system enables transparent multi-hop wireless" - " backhauls that are able to perform statistical analysis of" - " different kinds of data (temperature, humidity, etc.) " - "coming from a WSN while ensuring both end-to-end encryption" - "and hop-by-hop authentication.") + message = """ +In 2010 Riggio and Sicari presented a practical application of homomorphic +encryption to a hybrid wireless sensor/mesh network. The system enables +transparent multi-hop wireless backhauls that are able to perform statistical +analysis of different kinds of data (temperature, humidity, etc.) coming from +a WSN while ensuring both end-to-end encryption and hop-by-hop +authentication.""" encrypted = str(gpg.encrypt(message, dijk)) self.assertNotEqual(message, encrypted, "Data must have changed") @@ -659,50 +659,69 @@ class GPGTestCase(unittest.TestCase): key = self.generate_key("Marten van Dijk", "xorr.ox") dijk = key.fingerprint gpg = self.gpg - message = ("In 2010 Riggio and Sicari presented a practical application" - " of homomorphic encryption to a hybrid wireless sensor/mesh" - " network. The system enables transparent multi-hop wireless" - " backhauls that are able to perform statistical analysis of" - " different kinds of data (temperature, humidity, etc.) " - "coming from a WSN while ensuring both end-to-end encryption" - "and hop-by-hop authentication.") + message = """ +In 2010 Riggio and Sicari presented a practical application of homomorphic +encryption to a hybrid wireless sensor/mesh network. The system enables +transparent multi-hop wireless backhauls that are able to perform statistical +analysis of different kinds of data (temperature, humidity, etc.) coming from +a WSN while ensuring both end-to-end encryption and hop-by-hop +authentication.""" encrypted2 = str(gpg.encrypt(message, [gentry, dijk])) self.assertNotEqual(message, encrypted2, "PT and CT should not match") def test_decryption(self): """Test decryption""" + key = self.generate_key("Frey", "fr.ey", + passphrase="craiggentry") + frey = key.fingerprint + key = self.generate_key("Rück", "rü.ck", + passphrase="ruck") + ruck = key.fingerprint + gpg = self.gpg + message = """ +In 2010 Riggio and Sicari presented a practical application of homomorphic +encryption to a hybrid wireless sensor/mesh network. The system enables +transparent multi-hop wireless backhauls that are able to perform statistical +analysis of different kinds of data (temperature, humidity, etc.) coming from +a WSN while ensuring both end-to-end encryption and hop-by-hop +authentication.""" + encrypted = str(self.gpg.encrypt(message, ruck)) + decrypted = self.gpg.decrypt(encrypted, passphrase="ruck") + if message != decrypted.data: + logger.debug("was: %r", message) + logger.debug("new: %r", decrypted.data) + self.assertEqual(message, decrypted.data) + + def test_decryption_multi_recipient(self): + """Test decryption of an encrypted string for multiple users""" key = self.generate_key("Craig Gentry", "xorr.ox", passphrase="craiggentry") gentry = key.fingerprint key = self.generate_key("Marten van Dijk", "xorr.ox") dijk = key.fingerprint - gpg = self.gpg - message = ("In 2010 Riggio and Sicari presented a practical application" - " of homomorphic encryption to a hybrid wireless sensor/mesh" - " network. The system enables transparent multi-hop wireless" - " backhauls that are able to perform statistical analysis of" - " different kinds of data (temperature, humidity, etc.) " - "coming from a WSN while ensuring both end-to-end encryption" - "and hop-by-hop authentication.") - encrypted = str(gpg.encrypt(message, dijk)) - decrypted = self.gpg.decrypt(encrypted, passphrase="martenvandijk") - if message != decrypted.data: - logger.debug("was: %r", message) - logger.debug("new: %r", decrypted.data) - self.assertEqual(message, decrypted.data, "Round-trip must work") + message = """ +In 2010 Riggio and Sicari presented a practical application of homomorphic +encryption to a hybrid wireless sensor/mesh network. The system enables +transparent multi-hop wireless backhauls that are able to perform statistical +analysis of different kinds of data (temperature, humidity, etc.) coming from +a WSN while ensuring both end-to-end encryption and hop-by-hop +authentication.""" + encrypted = str(self.gpg.encrypt(message, [gentry, dijk])) + self.assertNotEqual(message, encrypted, "PT and CT should not match") + decrypted1 = self.gpg.decrypt(encrypted, passphrase="craiggentry") + self.assertEqual(message, str(decrypted1.data)) + decrypted2 = self.gpg.decrypt(encrypted, passphrase="martenvandijk") + self.assertEqual(message, str(decrypted2.data)) - encrypted2 = str(gpg.encrypt(message, [gentry, dijk])) - self.assertNotEqual(message, encrypted2, "PT and CT should not match") - decrypted1 = gpg.decrypt(encrypted2, passphrase="craiggentry") - self.assertEqual(message, decrypted.data, "Round-trip must work") - decrypted2 = gpg.decrypt(encrypted2, passphrase="martenvandijk") - self.assertEqual(message, decrypted.data, "Round-trip must work") - # Test symmetric encryption - data = "chippy was here" - edata = str(gpg.encrypt(data, None, passphrase='bbrown', - symmetric=True)) - decrypted = gpg.decrypt(edata, passphrase='bbrown') - self.assertEqual(data, str(decrypted)) + def test_symmetric_encryption_and_decryption(self): + """Test symmetric encryption and decryption""" + msg = """ +If you have something that you don't want anyone to know, maybe you shouldn't +be doing it in the first place. - Eric Schmidt, CEO of Google""" + encrypted = str(self.gpg.encrypt(msg, None, passphrase='quiscustodiet', + symmetric=True)) + decrypted = self.gpg.decrypt(encrypted, passphrase='quiscustodiet') + self.assertEqual(msg, str(decrypted.data)) def test_file_encryption_and_decryption(self): """Test that encryption/decryption to/from file works.""" @@ -777,6 +796,8 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_encryption_alt_encoding', 'test_encryption_multi_recipient', 'test_decryption', + 'test_decryption_multi_recipient', + 'test_symmetric_encryption_and_decryption', 'test_file_encryption_and_decryption']), 'listkeys': set(['test_list_keys_after_generation']), 'keyrings': set(['test_public_keyring', From ae0ab0a7c7269238ee0d91997219766b3ffe1f35 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 18:22:50 +0000 Subject: [PATCH 214/397] WHO PUTS UNITTESTS IN A TRY/EXCEPT BLOCK WHICH CATCHES ALL EXCEPTIONS?! * WHAT WAS THE POINT OF WRITING THE UNITTEST IF IT ALWAYS PASSES? --- gnupg/tests/test_gnupg.py | 42 ++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index 715172c..17068ab 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -732,29 +732,25 @@ be doing it in the first place. - Eric Schmidt, CEO of Google""" # On Windows, if the handles aren't closed, the files can't be deleted #os.close(encfno) #os.close(decfno) - try: - key = self.generate_key("Andrew Able", "alpha.com", - passphrase="andy") - andrew = key.fingerprint - key = self.generate_key("Barbara Brown", "beta.com") - barbara = key.fingerprint - data = "Hello, world!" - file = util._make_binary_stream(data, self.gpg.encoding) - edata = self.gpg.encrypt_file(file, barbara, - armor=False, output=encfname) - ddata = self.gpg.decrypt_file(efile, passphrase="bbrown", - output=decfname) - encfname.seek(0, 0) # can't use os.SEEK_SET in 2.4 - edata = encfname.read() - ddata = decfname.read() - data = data.encode(self.gpg.encoding) - if ddata != data: - logger.debug("was: %r", data) - logger.debug("new: %r", ddata) - self.assertEqual(data, ddata, "Round-trip must work") - except Exception as exc: - logger.warn(exc.message) - logger.debug("test_file_encryption_and_decryption ends") + key = self.generate_key("Andrew Able", "alpha.com", + passphrase="andy") + andrew = key.fingerprint + key = self.generate_key("Barbara Brown", "beta.com") + barbara = key.fingerprint + data = "Hello, world!" + file = util._make_binary_stream(data, self.gpg.encoding) + edata = self.gpg.encrypt_file(file, barbara, + armor=False, output=encfname) + ddata = self.gpg.decrypt_file(efile, passphrase="bbrown", + output=decfname) + encfname.seek(0, 0) # can't use os.SEEK_SET in 2.4 + edata = encfname.read() + ddata = decfname.read() + data = data.encode(self.gpg.encoding) + if ddata != data: + logger.debug("was: %r", data) + logger.debug("new: %r", ddata) + self.assertEqual(data, ddata) suites = { 'parsers': set(['test_parsers_fix_unsafe', From cfd7b3824214a9f8d96aa575deec09fd9d740234 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 18:25:17 +0000 Subject: [PATCH 215/397] Add option to delete subkeys with GPG.delete_keys(). * TODO this needs unittests. --- gnupg/gnupg.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index db629be..edcf0c6 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -524,7 +524,7 @@ class GPG(object): logger.debug('recv_keys result: %r', result.__dict__) return result - def delete_keys(self, fingerprints, secret=False): + def delete_keys(self, fingerprints, secret=False, subkeys=False): """Delete a key, or list of keys, from the current keyring. The keys must be refered to by their full fingerprint for GnuPG to @@ -538,11 +538,17 @@ class GPG(object): :param bool secret: If True, delete the corresponding secret key(s) also. (default: False) + :param bool subkeys: If True, delete the secret subkey first, then + the public key. Same as + ``gpg --delete-secret-and-public-key 0x12345678`` + (default: False) """ which='keys' if secret: which='secret-key' + if subkeys: + which='secret-and-public-key' if _util._is_list_or_tuple(fingerprints): fingerprints = ' '.join(fingerprints) From cc7beb7185dd36bf5b990461907d30269faf96e0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 18:26:14 +0000 Subject: [PATCH 216/397] Rewrite GPG.decrypt_file(). --- gnupg/gnupg.py | 67 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index edcf0c6..d2ee5c6 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -868,16 +868,18 @@ class GPG(object): if testing: out += "%no-protection\n" out += "%transient-key\n" - + out += "%commit\n" return out # # ENCRYPTION # - def encrypt_file(self, file, recipients, sign=None, - always_trust=False, passphrase=None, - armor=True, output=None, symmetric=False): + def encrypt_file(self, file, recipients, sign_with=None, + always_trust=False, passphrase=None, armor=True, + output=None, encrypt=True, symmetric=False, + cipher_algo='AES256', digest_algo='SHA512', + compress_algo='ZLIB'): """Encrypt the message read from ``file``. :type file: file or :class:BytesIO @@ -885,8 +887,8 @@ class GPG(object): :type recipients: str or list or tuple :param recipients: The recipients to encrypt to. Recipients may be specified by UID or keyID/fingerprint. - :param str sign: The keyID to use for signing, i.e. - "gpg --sign --default-key A3ADB67A2CDB8B35 ..." + :param str sign_with: The keyID to use for signing, i.e. + "gpg --sign --default-key A3ADB67A2CDB8B35 ..." :param bool always_trust: If True, ignore trust warnings on recipient keys. If False, display trust warnings. (default: False) @@ -900,29 +902,48 @@ class GPG(object): :param str output: The output file to write to. If not specified, the encrypted output is returned, and thus should be stored as an object in Python. For example: - - >>> gpg = gnupg.GPG(gpghome='./tmp_test') - """ - if output: # write the output to a file with the specified name - if os.path.exists(output): - os.remove(output) # to avoid overwrite confirmation message - args.append('--output "%s"' % output) - args = ['--encrypt'] + + args = list() + + ## both can be used at the same time for an encrypted file which + ## is decryptable with a passphrase or secretkey. + if encrypt: + args.append('--encrypt') if symmetric: - args = ['--symmetric'] - else: - args = ['--encrypt'] - if not _util._is_list_or_tuple(recipients): + args.append('--symmetric') + + if not _util._is_list_or_tuple(recipients): + if isinstance(recipients, str): + recipients = [rec for rec in recipients.split(' ')] + else: recipients = (recipients,) - for recipient in recipients: - args.append('--recipient "%s"' % recipient) + if len(recipients) > 1: + args.append('--multifile') + for recipient in recipients: + args.append('--recipient %s' % recipient) + + if output is not None: + if getattr(output, 'fileno', None) is not None: + if os.path.exists(output): + os.remove(output) # to avoid overwrite confirmation message + args.append('--output "%s"' % output) + if armor: args.append('--armor') - if sign: - args.append('--sign --default-key "%s"' % sign) + if sign_with: + args.append('--sign') + args.append('--default-key %s' % sign_with) + if digest_algo: + args.append('--digest-algo %s' % digest_algo) if always_trust: - args.append("--always-trust") + args.append('--always-trust') + + if cipher_algo: + args.append('--cipher-algo %s' % cipher_algo) + if compress_algo: + args.append('--compress-algo %s' % compress_algo) + result = self._result_map['crypt'](self) self._handle_io(args, file, result, passphrase=passphrase, binary=True) logger.debug('encrypt result: %r', result.data) From 0785d7dd3b5aeb432639d0a3a34d350afcf3ccef Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 18:28:04 +0000 Subject: [PATCH 217/397] Various docstring and whitespace fixes in parsers.py. --- gnupg/parsers.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 92566e4..5cca438 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -45,8 +45,7 @@ class UsageError(Exception): 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 quotes if they exist. Regex pilfered from python-3.x shlex module. :param str shell_input: The input intended for the GnuPG process. @@ -512,7 +511,8 @@ def _sanitise_list(arg_list): class Verify(object): - """Parser for internal status messages from GnuPG for ``--verify``.""" + """Parser for internal status messages from GnuPG for ``--verify``. + """ TRUST_UNDEFINED = 0 TRUST_NEVER = 1 @@ -540,7 +540,7 @@ class Verify(object): pubkey_fingerprint = None #: The keyid of the signing key. key_id = None - #: xxx I'm not sure how this is different to key_id. + #: The id of the signature itself. signature_id = None #: The creation date of the signing key. creation_date = None @@ -569,8 +569,8 @@ class Verify(object): self.trust_text = key self.trust_level = self.TRUST_LEVELS[key] elif key in ("RSA_OR_IDEA", "NODATA", "IMPORT_RES", "PLAINTEXT", - "PLAINTEXT_LENGTH", "POLICY_URL", "DECRYPTION_INFO", - "DECRYPTION_OKAY", "INV_SGNR"): + "PLAINTEXT_LENGTH", "POLICY_URL", "DECRYPTION_INFO", + "DECRYPTION_OKAY", "INV_SGNR"): pass elif key == "BADSIG": self.valid = False @@ -621,8 +621,8 @@ class Verify(object): class Crypt(Verify): - """Parser for internal status messages from GnuPG for ``--encrypt`` and - ``--decrypt``. + """Parser for internal status messages from GnuPG for + ``--encrypt````--decrypt``, and ``--decrypt-files``. """ def __init__(self, gpg): super(Crypt, self).__init__(gpg) From 425890036816e5b354ab07b7950baa0d148f1028 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 18:28:47 +0000 Subject: [PATCH 218/397] Fix a bug which caused result_map['crypt'] to expect a Verify(). --- gnupg/parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 5cca438..3707191 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -625,7 +625,7 @@ class Crypt(Verify): ``--encrypt````--decrypt``, and ``--decrypt-files``. """ def __init__(self, gpg): - super(Crypt, self).__init__(gpg) + self.gpg = gpg self.data = str() self.ok = False self.status = str() From e5ac938486edd6edf3b0ec21b14cb16cf095debd Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 18:30:22 +0000 Subject: [PATCH 219/397] Add hastily written check_preferences(). * TODO I sleepily realised two lines before the end of bashing this out that it should just be a set difference. --- gnupg/parsers.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 3707191..ab10a0c 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -44,6 +44,33 @@ class UsageError(Exception): """Raised when incorrect usage of the API occurs..""" +def _check_preferences(prefs, pref_type=None): + cipher = frozenset(['AES256', 'AES192', 'CAMELLIA256', 'CAMELLIA192', + 'TWOFISH',]) + digest = frozenset(['SHA512', 'SHA384', 'SHA256', 'SHA224']) + compress = frozenset(['ZLIB', 'ZIP', 'Uncompressed']) + all = frozenset([ciphers, hashes, compress]) + + if isinstance(prefs, str): + prefs = prefs.split(' ') + if not pref_type: + pref_type = all + + ## xxx we should use set differences + if pref_type == 'cipher': + for pref in prefs: + if not cipher.contains(pref): return + if pref_type == 'digest': + for pref in prefs: + if not digest.contains(pref): return + if pref_type == 'compress': + for pref in prefs: + if not compress.contains(pref): return + if pref_type == 'all': + for pref in prefs: + if not all.contains(pref): return + return ' '.join([pref for pref in prefs]) + def _fix_unsafe(shell_input): """Find characters used to escape from a string into a shell, and wrap them in quotes if they exist. Regex pilfered from python-3.x shlex module. From 2700b3c3388473fb8ca73566683152b21534ffee Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 18:32:18 +0000 Subject: [PATCH 220/397] Return the result of a encryption or decryption operation without *.data --- gnupg/parsers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index ab10a0c..a3e9a35 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -666,6 +666,9 @@ class Crypt(Verify): def __str__(self): return self.data.decode(self.gpg.encoding, self.gpg._decode_errors) + def __repr__(self): + return str(self) + def _handle_status(self, key, value): """Parse a status code from the attached GnuPG process. From e3eb7eea0c847fe0dc585099595212a8bdac4d99 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 18:33:39 +0000 Subject: [PATCH 221/397] Fix an old-style class. --- gnupg/parsers.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index a3e9a35..4ddd686 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -1079,9 +1079,7 @@ class Verify(object): else: raise ValueError("Unknown status message: %r" % key) -## xxx old style class - -class ListPackets(): +class ListPackets(object): """ Handle status messages for --list-packets. """ From 25637ef50ea991791a64a784e0cd3991386e28bd Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 11 May 2013 18:34:28 +0000 Subject: [PATCH 222/397] PEP-8ify a couple lines that were running off the screen. * Also, change a logger string to say 'GnuPG' instead of the binary's name 'gpg'. --- gnupg/util.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/gnupg/util.py b/gnupg/util.py index a4ddcfe..f07026b 100644 --- a/gnupg/util.py +++ b/gnupg/util.py @@ -95,14 +95,16 @@ def _copy_data(instream, outstream): if len(data) == 0: break sent += len(data) - logger.debug("_copy_data(): sending chunk (%d):\n%s" % (sent, data[:256])) + logger.debug("_copy_data(): sending chunk (%d):\n%s" + % (sent, data[:256])) try: outstream.write(data) except UnicodeError: try: outstream.write(data.encode(enc)) except IOError: - logger.exception('_copy_data(): Error sending data: Broken pipe') + logger.exception( + '_copy_data(): Error sending data: Broken pipe') break except IOError: # Can get 'broken pipe' errors even when all data was sent @@ -161,7 +163,7 @@ def _find_binary(binary=None): except IndexError as ie: logger.debug(ie.message) if binary is None: try: binary = _which('gpg')[0] - except IndexError: raise RuntimeError("gpg is not installed") + except IndexError: raise RuntimeError("GnuPG is not installed!") try: assert os.path.isabs(binary), "Path to gpg binary not absolute" assert not os.path.islink(binary), "Path to gpg binary is symlink" From c97b51fec5a597b0b708241ddd1ec1e0948d68d3 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 12 May 2013 09:32:46 +0000 Subject: [PATCH 223/397] Add versioneer and restructure the repo into a src/ dir. --- Makefile | 2 +- gnupg/__init__.py | 19 - gnupg/tests/__init__.py | 0 report-a-bug | 260 ------- setup.py | 15 +- src/__init__.py | 23 + src/_version.py | 197 ++++++ {gnupg => src}/copyleft.py | 0 {gnupg => src}/gnupg.py | 5 - {gnupg => src}/parsers.py | 3 +- {gnupg => src}/util.py | 3 +- .../files/cypherpunk_manifesto | 0 {gnupg/tests => tests}/test_gnupg.py | 5 +- versioneer.py | 656 ++++++++++++++++++ 14 files changed, 890 insertions(+), 298 deletions(-) delete mode 100644 gnupg/__init__.py delete mode 100644 gnupg/tests/__init__.py delete mode 100644 report-a-bug create mode 100644 src/__init__.py create mode 100644 src/_version.py rename {gnupg => src}/copyleft.py (100%) rename {gnupg => src}/gnupg.py (99%) rename {gnupg => src}/parsers.py (99%) rename {gnupg => src}/util.py (99%) rename {gnupg/tests => tests}/files/cypherpunk_manifesto (100%) rename {gnupg/tests => tests}/test_gnupg.py (99%) create mode 100644 versioneer.py diff --git a/Makefile b/Makefile index 69283ae..4c562fd 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ uninstall: cat installed-files.txt | sudo xargs rm -rf cleandocs: - sphinx-apidoc -F -A "Isis Agora Lovecruft" -H "python-gnupg" -V 0.4.0 -R 0.4.0 -o docs gnupg/ tests/ + sphinx-apidoc -F -A "Isis Agora Lovecruft" -H "python-gnupg" -V 0.4.0 -R 0.4.0 -o docs src/ tests/ docs: cd docs diff --git a/gnupg/__init__.py b/gnupg/__init__.py deleted file mode 100644 index 726038e..0000000 --- a/gnupg/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ - -__author__ = 'Isis Agora Lovecruft' -__contact__ = 'isis@leap.se' -__date__ = '1 April 2013' -__url__ = 'https://github.com/isislovecruft/python-gnupg' -__version__ = '0.4.0' -__license__ = 'AGPLv3' - -from copyleft import disclaimer as copyright -from copyleft import txcopyright - -import gnupg -from parsers import Crypt, DeleteResult, ListKeys -from parsers import GenKey, Sign, ImportResult, Verify -from gnupg import GPG - -__all__ = ["gnupg", "copyright", - "Crypt", "DeleteResult", "ListKeys", - "GenKey", "Sign", "Encrypt", "ImportResult", "Verify"] diff --git a/gnupg/tests/__init__.py b/gnupg/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/report-a-bug b/report-a-bug deleted file mode 100644 index 73e7b98..0000000 --- a/report-a-bug +++ /dev/null @@ -1,260 +0,0 @@ -#!/usr/bin/env python -#-*- coding: utf-8 -*- -''' - report-a-bug - ------------ - File a bug against python-gnupg. - - :authors: Wade Leftwich, - Will Holcomb, - Isis Lovecruft, - :license: AGPLv3, see LICENSE and COPYRIGHT files - :copyright: Ā© 2002-2013 Wade Leftwich - Ā© 2006 Will Holcomb - Ā© 2013 Isis Agora Lovecruft - :date: 11 April 2013 - :version: 0.0.1 - -''' - -from __future__ import print_function -from cStringIO import StringIO -from datetime import datetime - -import cookielib -import mimetools -import mimetypes -#import httplib -import os -import stat -import tempfile -import urllib -import urllib2 - -# Controls how sequences are uncoded. If true, elements may be given multiple -# values by assigning a sequence. -doseq = 1 - -def _has_py3k(): - """Check if we're running on Python>=3.0.""" - try: - unicode - return False - except NameError: - return True - -## patch the stupid Python2.x input() problem: -if not _has_py3k(): - input = raw_input - -def _today(): - """Get the current date as a string in the form %Y-%m-%d.""" - now_string = datetime.now().__str__() - return now_string.split(' ', 1)[0] - -def _create_upload_list(): - """Create a dictionary containing information about files to upload.""" - - upload_list = list() - - WANT_UPLOAD = True - FILE_NUMBER = 1 - FILENAME_FIELD = 'attachments[' + str(FILE_NUMBER) + '][file]' - FILEDESC_FIELD = 'attachments[' + str(FILE_NUMBER) + '][description]' - - while WANT_UPLOAD: - do_upload = input("Would you like to attach a file to this ticket? " - + "(y/N) ") - if do_upload.strip() == "": - WANT_UPLOAD = False - break - else: - WANT_UPLOAD = True - - upload = input("Please specify the file to upload, as a filesystem " - + "absolute path or as relative to this directory (%s): " - % os.getcwd()).strip() - - if len(upload) > 0: - if upload.startswith('~'): - upload = os.path.expanduser(upload) - if not os.path.isabs(upload): - upload = os.path.abspath(upload) - try: - assert os.path.isfile(upload), "is not a file" - except AssertionError as ae: - print("Skipping: '%s' %s" % (upload, ae.message)) - else: - upload_fields = {'fields': [FILENAME_FIELD, FILEDESC_FIELD], - 'filepath': upload,} - upload_list.append(upload_fields) - FILE_NUMBER += 1 - return upload_list - -def _create_fields_and_headers(host, url, assign_to=None, - category=None, target_version=None): - REPO_NAME = os.getcwd().rsplit(os.path.sep, 1)[1] - - subject = input("Please provide a brief subject line for the ticket: ") - subject = REPO_NAME + ": " + subject - descript = input("Ticket description:\n ") - whatisit = input("Is this a feature request or a bug report? " - + "(1=feature, 2=bug) ") - if whatisit not in ['1', '2']: - whatisit = '2' - serious = input("How important is this? (1=important, 2=normal, 3=trivial) ") - if serious not in ['1', '2', '3']: - serious = '2' - - headers = { - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', - 'Accept-Encoding': 'gzip, deflate', - 'Accept-Language': 'en-us,en-gb;q=0.9,en;', - 'Connection': 'keep-alive', - 'DNT': '1', - 'Host': host, - 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; rv:10.0) Gecko/20100101 Firefox/10.0', } - - fields = { - 'issue[tracker_id]': whatisit, - 'issue[subject]': subject, - 'issue[description]': descript, - 'issue[status_id]': '2', - 'issue[priority_id]': serious, - 'issue[assigned_to_id]': assign_to, - 'issue[category_id]': category, - 'issue[fixed_version_id]': target_version, - 'issue[start_date]': _today(), - 'issue[due_date]': '', - 'issue[estimated_hours]': '', - 'issue[done_ratio]': '0', - 'issue[custom_field_values][3]': '', - 'issue[custom_field_values][4]': '', - 'issue[custom_field_values][5]': '0', - 'send_notification': '0', - 'send_notification': '1', - 'commit': 'Create', } - - return fields, headers - - -class Callable: - def __init__(self, anycallable): - self.__call__ = anycallable - -class MultipartPostHandler(urllib2.BaseHandler): - handler_order = urllib2.HTTPHandler.handler_order - 10 # needs to run first - - def http_request(self, request): - data = request.get_data() - if data is not None and type(data) != str: - v_files = [] - v_vars = [] - try: - for (key, value) in data.items(): - if type(value) == file: - v_files.append((key, value)) - else: - v_vars.append((key, value)) - except TypeError: - systype, value, traceback = sys.exc_info() - raise TypeError("not non-string sequence or mapping object\n%s" - % traceback) - - if len(v_files) == 0: - data = urllib.urlencode(v_vars, doseq) - else: - boundary, data = self.multipart_encode(v_vars, v_files) - contenttype = 'multipart/form-data; boundary=%s' % boundary - if (request.has_header('Content-Type') and request.get_header( - 'Content-Type').find('multipart/form-data') != 0): - print("Replacing %s with %s" - % (request.get_header('content-type'), - 'multipart/form-data')) - request.add_unredirected_header('Content-Type', contenttype) - request.add_data(data) - return request - - def multipart_encode(self, fields=None, upload_list=None, - boundary=None, buf=None): - if fields is None: - fields = self.fields - if upload_list is None: - upload_list = self.upload_list - if boundary is None: - boundary = mimetools.choose_boundary() - if buf is None: - buf = StringIO() - - for (key, value) in fields: - buf.write('--%s\r\n' % boundary) - buf.write('Content-Disposition: form-data; name="%s"' % key) - buf.write('\r\n\r\n' + value + '\r\n') - - if isinstance(upload_list, list) and len(upload_list) > 0: - for upload in upload_list: - for (name, filepath) in upload: - with open(filepath) as fd: - file_size = os.fstat(fd.fileno())[stat.ST_SIZE] - filename = fd.name.split('/')[-1] - contenttype = mimetypes.guess_type(filename)[0] \ - or 'application/octet-stream' - buf.write('--%s\r\n' % boundary) - buf.write('Content-Disposition: form-data; ') - buf.write('name="%s"; filename="%s"' - % (name[0], filename)) - buf.write('Content-Type: %s\r\n' % contenttype) - # buf.write('Content-Length: %s\r\n' % file_size) - fd.seek(0) - buf.write('\r\n' + fd.read() + '\r\n') - buf.write('--' + boundary + '--\r\n\r\n') - buf.write('--%s\r\n' % boundary) - buf.write('Content-Disposition: form-data; ') - buf.write('name="%s"; filename="%s"' - % (name[1], filename)) - buf.write('\r\n\r\n') - buf.write('--' + boundary + '--\r\n\r\n') - - buf = buf.getvalue() - return boundary, buf - multipart_encode = Callable(multipart_encode) - - https_request = http_request - - -if __name__ == "__main__": - - raise SystemExit("Please fix me! This script needs a login handler.\n" - + "Everything else is finished.") - - ## if you're reusing this script please change these! - host = 'leap.se' - selector = '/code/projects/eip-server/issues/new' - assign_to = '30' ## isis - category = '26' ## email - target_version = '29' ## the close future - - url = 'https://' + host + selector - fields, headers = _create_fields_and_headers(host, selector, assign_to, - category, target_version) - upload_list = _create_upload_list() - cookies = cookielib.CookieJar() - opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), - MultipartPostHandler()) - urllib2.install_opener(opener) - - temp = tempfile.mkstemp(suffix=".html") - temp1 = tempfile.mkstemp(suffix=".html") - os.write(temp[0], opener.open('https://'+host+'/code/login', - {'username': 'cypherpunks', - 'password': 'writecode'}).read()) - - for index,upload in enumerate(upload_list): - for (field, filepath) in upload: - fields['file'+'-'+index] = open(filepath, 'rb') - res = opener.open(url, fields) - print("Posted to: %s" % res.geturl()) - print("Server response: %s " % res.code) - print(res.info()) - print(res.read()) - os.remove(temp[1]) diff --git a/setup.py b/setup.py index 03a62f1..036d57b 100644 --- a/setup.py +++ b/setup.py @@ -3,11 +3,14 @@ from distutils.core import setup -__module__ = 'gnupg' -__version__ = "0.4.0" +import versioneer +versioneer.versionfile_source = 'src/_version.py' +versioneer.versionfile_build = 'gnupg/_version.py' +versioneer.tag_prefix = 'python-gnupg-' +versioneer.parentdir_prefix = 'python-gnupg-' + __author__ = "Isis Agora Lovecruft" __contact__ = 'isis@leap.se' -__date__ = "1 April 2013" setup(name = "python-gnupg", description="A wrapper for the Gnu Privacy Guard (GPG or GnuPG)", @@ -15,13 +18,15 @@ setup(name = "python-gnupg", management, encryption and signature functionality from Python programs. \ It is intended for use with Python 2.6 or greater.", license="""Copyright Ā© 2013 Isis Lovecruft, et.al. see LICENSE file.""", - version=__version__, + version=versioneer.get_version(), + cmdclass=versioneer.get_cmdclass(), author=__author__, author_email=__contact__, maintainer=__author__, maintainer_email=__contact__, url="https://github.com/isislovecruft/python-gnupg", - packages=['gnupg', 'gnupg.tests'], + package_dir={'gnupg': 'src'}, + packages=['gnupg'], platforms="Linux, BSD, OSX, Windows", download_url="https://github.com/isislovecruft/python-gnupg/archive/develop.zip", classifiers=[ diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..92998b8 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,23 @@ +#-*- encoding: utf-8 -*- + +from .copyleft import disclaimer as copyright +from .copyleft import txcopyright + +import gnupg +from parsers import Crypt, DeleteResult, ListKeys +from parsers import GenKey, Sign, ImportResult, Verify +from gnupg import GPG + +from ._version import get_versions +__version__ = get_versions()['version'] +del get_versions + +gnupg.__version__ = __version__ +gnupg.__author__ = 'Isis Agora Lovecruft' +gnupg.__contact__ = 'isis@leap.se' +gnupg.__url__ = 'https://github.com/isislovecruft/python-gnupg' +gnupg.__license__ = copyright + +__all__ = ["gnupg", "copyright", + "Crypt", "DeleteResult", "ListKeys", + "GenKey", "Sign", "Encrypt", "ImportResult", "Verify"] diff --git a/src/_version.py b/src/_version.py new file mode 100644 index 0000000..42d41a2 --- /dev/null +++ b/src/_version.py @@ -0,0 +1,197 @@ + +IN_LONG_VERSION_PY = True +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (build by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by +# versioneer-0.7+ (https://github.com/warner/python-versioneer) + +# these strings will be replaced by git during git-archive +git_refnames = "$Format:%d$" +git_full = "$Format:%H$" + + +import subprocess +import sys + +def run_command(args, cwd=None, verbose=False): + try: + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd) + except EnvironmentError: + e = sys.exc_info()[1] + if verbose: + print("unable to run %s" % args[0]) + print(e) + return None + stdout = p.communicate()[0].strip() + if sys.version >= '3': + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % args[0]) + return None + return stdout + + +import sys +import re +import os.path + +def get_expanded_variables(versionfile_source): + # the code embedded in _version.py can just fetch the value of these + # variables. When used from setup.py, we don't want to import + # _version.py, so we do it with a regexp instead. This function is not + # used from _version.py. + variables = {} + try: + for line in open(versionfile_source,"r").readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["full"] = mo.group(1) + except EnvironmentError: + pass + return variables + +def versions_from_expanded_variables(variables, tag_prefix, verbose=False): + refnames = variables["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("variables are unexpanded, not using") + return {} # unexpanded, so not in an unpacked git-archive tarball + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + for ref in list(refs): + if not re.search(r'\d', ref): + if verbose: + print("discarding '%s', no digits" % ref) + refs.discard(ref) + # Assume all version tags have a digit. git's %d expansion + # behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us + # distinguish between branches and tags. By ignoring refnames + # without digits, we filter out many common branch names like + # "release" and "stabilization", as well as "HEAD" and "master". + if verbose: + print("remaining refs: %s" % ",".join(sorted(refs))) + for ref in sorted(refs): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %s" % r) + return { "version": r, + "full": variables["full"].strip() } + # no suitable tags, so we use the full revision id + if verbose: + print("no suitable tags, using full revision id") + return { "version": variables["full"].strip(), + "full": variables["full"].strip() } + +def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): + # this runs 'git' from the root of the source tree. That either means + # someone ran a setup.py command (and this code is in versioneer.py, so + # IN_LONG_VERSION_PY=False, thus the containing directory is the root of + # the source tree), or someone ran a project-specific entry point (and + # this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the + # containing directory is somewhere deeper in the source tree). This only + # gets called if the git-archive 'subst' variables were *not* expanded, + # and _version.py hasn't already been rewritten with a short version + # string, meaning we're inside a checked out source tree. + + try: + here = os.path.abspath(__file__) + except NameError: + # some py2exe/bbfreeze/non-CPython implementations don't do __file__ + return {} # not always correct + + # versionfile_source is the relative path from the top of the source tree + # (where the .git directory might live) to this file. Invert this to find + # the root from __file__. + root = here + if IN_LONG_VERSION_PY: + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + root = os.path.dirname(here) + if not os.path.exists(os.path.join(root, ".git")): + if verbose: + print("no .git in %s" % root) + return {} + + GIT = "git" + if sys.platform == "win32": + GIT = "git.cmd" + stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"], + cwd=root) + if stdout is None: + return {} + if not stdout.startswith(tag_prefix): + if verbose: + print("tag '%s' doesn't start with prefix '%s'" % (stdout, tag_prefix)) + return {} + tag = stdout[len(tag_prefix):] + stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root) + if stdout is None: + return {} + full = stdout.strip() + if tag.endswith("-dirty"): + full += "-dirty" + return {"version": tag, "full": full} + + +def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False): + if IN_LONG_VERSION_PY: + # We're running from _version.py. If it's from a source tree + # (execute-in-place), we can work upwards to find the root of the + # tree, and then check the parent directory for a version string. If + # it's in an installed application, there's no hope. + try: + here = os.path.abspath(__file__) + except NameError: + # py2exe/bbfreeze/non-CPython don't have __file__ + return {} # without __file__, we have no hope + # versionfile_source is the relative path from the top of the source + # tree to _version.py. Invert this to find the root from __file__. + root = here + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + # we're running from versioneer.py, which means we're running from + # the setup.py in a source tree. sys.argv[0] is setup.py in the root. + here = os.path.abspath(sys.argv[0]) + root = os.path.dirname(here) + + # Source tarballs conventionally unpack into a directory that includes + # both the project name and a version string. + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): + if verbose: + print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" % + (root, dirname, parentdir_prefix)) + return None + return {"version": dirname[len(parentdir_prefix):], "full": ""} + +tag_prefix = "python-gnupg-" +parentdir_prefix = "python-gnupg-" +versionfile_source = "src/_version.py" + +def get_versions(default={"version": "unknown", "full": ""}, verbose=False): + variables = { "refnames": git_refnames, "full": git_full } + ver = versions_from_expanded_variables(variables, tag_prefix, verbose) + if not ver: + ver = versions_from_vcs(tag_prefix, versionfile_source, verbose) + if not ver: + ver = versions_from_parentdir(parentdir_prefix, versionfile_source, + verbose) + if not ver: + ver = default + return ver + diff --git a/gnupg/copyleft.py b/src/copyleft.py similarity index 100% rename from gnupg/copyleft.py rename to src/copyleft.py diff --git a/gnupg/gnupg.py b/src/gnupg.py similarity index 99% rename from gnupg/gnupg.py rename to src/gnupg.py index d2ee5c6..9d73511 100644 --- a/gnupg/gnupg.py +++ b/src/gnupg.py @@ -75,11 +75,6 @@ Vinay Sajip's documentation: """ -__author__ = "Isis Agora Lovecruft" -__module__ = 'gnupg' -__version__ = "0.4.0" - - try: from io import StringIO from io import BytesIO diff --git a/gnupg/parsers.py b/src/parsers.py similarity index 99% rename from gnupg/parsers.py rename to src/parsers.py index 4ddd686..4477708 100644 --- a/gnupg/parsers.py +++ b/src/parsers.py @@ -22,10 +22,9 @@ parsers.py Classes for parsing GnuPG status messages and sanitising commandline options. ''' -from gnupg import __author__ -from gnupg import __version__ __module__ = 'gnupg.parsers' + import logging import re diff --git a/gnupg/util.py b/src/util.py similarity index 99% rename from gnupg/util.py rename to src/util.py index f07026b..4ddbc65 100644 --- a/gnupg/util.py +++ b/src/util.py @@ -22,10 +22,9 @@ utils.py Extra utilities for python-gnupg. ''' -from gnupg import __author__ -from gnupg import __version__ __module__ = 'gnupg.util' + from datetime import datetime import logging diff --git a/gnupg/tests/files/cypherpunk_manifesto b/tests/files/cypherpunk_manifesto similarity index 100% rename from gnupg/tests/files/cypherpunk_manifesto rename to tests/files/cypherpunk_manifesto diff --git a/gnupg/tests/test_gnupg.py b/tests/test_gnupg.py similarity index 99% rename from gnupg/tests/test_gnupg.py rename to tests/test_gnupg.py index 17068ab..53f5b51 100644 --- a/gnupg/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -28,12 +28,9 @@ import gnupg from gnupg import parsers from gnupg import util -__author__ = gnupg.__author__ -__version__ = gnupg.__version__ - logger = logging.getLogger('gnupg') -_here = os.path.join(os.path.join(util._repo, 'gnupg'), 'tests') +_here = os.path.join(os.getcwd(), 'tests') _files = os.path.join(_here, 'files') _tempd = os.path.join(_here, 'tmp') diff --git a/versioneer.py b/versioneer.py new file mode 100644 index 0000000..57d9941 --- /dev/null +++ b/versioneer.py @@ -0,0 +1,656 @@ +#! /usr/bin/python + +"""versioneer.py + +(like a rocketeer, but for versions) + +* https://github.com/warner/python-versioneer +* Brian Warner +* License: Public Domain +* Version: 0.7+ + +This file helps distutils-based projects manage their version number by just +creating version-control tags. + +For developers who work from a VCS-generated tree (e.g. 'git clone' etc), +each 'setup.py version', 'setup.py build', 'setup.py sdist' will compute a +version number by asking your version-control tool about the current +checkout. The version number will be written into a generated _version.py +file of your choosing, where it can be included by your __init__.py + +For users who work from a VCS-generated tarball (e.g. 'git archive'), it will +compute a version number by looking at the name of the directory created when +te tarball is unpacked. This conventionally includes both the name of the +project and a version number. + +For users who work from a tarball built by 'setup.py sdist', it will get a +version number from a previously-generated _version.py file. + +As a result, loading code directly from the source tree will not result in a +real version. If you want real versions from VCS trees (where you frequently +update from the upstream repository, or do new development), you will need to +do a 'setup.py version' after each update, and load code from the build/ +directory. + +You need to provide this code with a few configuration values: + + versionfile_source: + A project-relative pathname into which the generated version strings + should be written. This is usually a _version.py next to your project's + main __init__.py file. If your project uses src/myproject/__init__.py, + this should be 'src/myproject/_version.py'. This file should be checked + in to your VCS as usual: the copy created below by 'setup.py + update_files' will include code that parses expanded VCS keywords in + generated tarballs. The 'build' and 'sdist' commands will replace it with + a copy that has just the calculated version string. + + versionfile_build: + Like versionfile_source, but relative to the build directory instead of + the source directory. These will differ when your setup.py uses + 'package_dir='. If you have package_dir={'myproject': 'src/myproject'}, + then you will probably have versionfile_build='myproject/_version.py' and + versionfile_source='src/myproject/_version.py'. + + tag_prefix: a string, like 'PROJECTNAME-', which appears at the start of all + VCS tags. If your tags look like 'myproject-1.2.0', then you + should use tag_prefix='myproject-'. If you use unprefixed tags + like '1.2.0', this should be an empty string. + + parentdir_prefix: a string, frequently the same as tag_prefix, which + appears at the start of all unpacked tarball filenames. If + your tarball unpacks into 'myproject-1.2.0', this should + be 'myproject-'. + +To use it: + + 1: include this file in the top level of your project + 2: make the following changes to the top of your setup.py: + import versioneer + versioneer.versionfile_source = 'src/myproject/_version.py' + versioneer.versionfile_build = 'myproject/_version.py' + versioneer.tag_prefix = '' # tags are like 1.2.0 + versioneer.parentdir_prefix = 'myproject-' # dirname like 'myproject-1.2.0' + 3: add the following arguments to the setup() call in your setup.py: + version=versioneer.get_version(), + cmdclass=versioneer.get_cmdclass(), + 4: run 'setup.py update_files', which will create _version.py, and will + append the following to your __init__.py: + from _version import __version__ + 5: modify your MANIFEST.in to include versioneer.py + 6: add both versioneer.py and the generated _version.py to your VCS +""" + +import os, sys, re +from distutils.core import Command +from distutils.command.sdist import sdist as _sdist +from distutils.command.build import build as _build + +versionfile_source = None +versionfile_build = None +tag_prefix = None +parentdir_prefix = None + +VCS = "git" +IN_LONG_VERSION_PY = False + + +LONG_VERSION_PY = ''' +IN_LONG_VERSION_PY = True +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (build by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by +# versioneer-0.7+ (https://github.com/warner/python-versioneer) + +# these strings will be replaced by git during git-archive +git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" +git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" + + +import subprocess +import sys + +def run_command(args, cwd=None, verbose=False): + try: + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd) + except EnvironmentError: + e = sys.exc_info()[1] + if verbose: + print("unable to run %%s" %% args[0]) + print(e) + return None + stdout = p.communicate()[0].strip() + if sys.version >= '3': + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %%s (error)" %% args[0]) + return None + return stdout + + +import sys +import re +import os.path + +def get_expanded_variables(versionfile_source): + # the code embedded in _version.py can just fetch the value of these + # variables. When used from setup.py, we don't want to import + # _version.py, so we do it with a regexp instead. This function is not + # used from _version.py. + variables = {} + try: + for line in open(versionfile_source,"r").readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["full"] = mo.group(1) + except EnvironmentError: + pass + return variables + +def versions_from_expanded_variables(variables, tag_prefix, verbose=False): + refnames = variables["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("variables are unexpanded, not using") + return {} # unexpanded, so not in an unpacked git-archive tarball + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + for ref in list(refs): + if not re.search(r'\d', ref): + if verbose: + print("discarding '%%s', no digits" %% ref) + refs.discard(ref) + # Assume all version tags have a digit. git's %%d expansion + # behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us + # distinguish between branches and tags. By ignoring refnames + # without digits, we filter out many common branch names like + # "release" and "stabilization", as well as "HEAD" and "master". + if verbose: + print("remaining refs: %%s" %% ",".join(sorted(refs))) + for ref in sorted(refs): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %%s" %% r) + return { "version": r, + "full": variables["full"].strip() } + # no suitable tags, so we use the full revision id + if verbose: + print("no suitable tags, using full revision id") + return { "version": variables["full"].strip(), + "full": variables["full"].strip() } + +def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): + # this runs 'git' from the root of the source tree. That either means + # someone ran a setup.py command (and this code is in versioneer.py, so + # IN_LONG_VERSION_PY=False, thus the containing directory is the root of + # the source tree), or someone ran a project-specific entry point (and + # this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the + # containing directory is somewhere deeper in the source tree). This only + # gets called if the git-archive 'subst' variables were *not* expanded, + # and _version.py hasn't already been rewritten with a short version + # string, meaning we're inside a checked out source tree. + + try: + here = os.path.abspath(__file__) + except NameError: + # some py2exe/bbfreeze/non-CPython implementations don't do __file__ + return {} # not always correct + + # versionfile_source is the relative path from the top of the source tree + # (where the .git directory might live) to this file. Invert this to find + # the root from __file__. + root = here + if IN_LONG_VERSION_PY: + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + root = os.path.dirname(here) + if not os.path.exists(os.path.join(root, ".git")): + if verbose: + print("no .git in %%s" %% root) + return {} + + GIT = "git" + if sys.platform == "win32": + GIT = "git.cmd" + stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"], + cwd=root) + if stdout is None: + return {} + if not stdout.startswith(tag_prefix): + if verbose: + print("tag '%%s' doesn't start with prefix '%%s'" %% (stdout, tag_prefix)) + return {} + tag = stdout[len(tag_prefix):] + stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root) + if stdout is None: + return {} + full = stdout.strip() + if tag.endswith("-dirty"): + full += "-dirty" + return {"version": tag, "full": full} + + +def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False): + if IN_LONG_VERSION_PY: + # We're running from _version.py. If it's from a source tree + # (execute-in-place), we can work upwards to find the root of the + # tree, and then check the parent directory for a version string. If + # it's in an installed application, there's no hope. + try: + here = os.path.abspath(__file__) + except NameError: + # py2exe/bbfreeze/non-CPython don't have __file__ + return {} # without __file__, we have no hope + # versionfile_source is the relative path from the top of the source + # tree to _version.py. Invert this to find the root from __file__. + root = here + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + # we're running from versioneer.py, which means we're running from + # the setup.py in a source tree. sys.argv[0] is setup.py in the root. + here = os.path.abspath(sys.argv[0]) + root = os.path.dirname(here) + + # Source tarballs conventionally unpack into a directory that includes + # both the project name and a version string. + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): + if verbose: + print("guessing rootdir is '%%s', but '%%s' doesn't start with prefix '%%s'" %% + (root, dirname, parentdir_prefix)) + return None + return {"version": dirname[len(parentdir_prefix):], "full": ""} + +tag_prefix = "%(TAG_PREFIX)s" +parentdir_prefix = "%(PARENTDIR_PREFIX)s" +versionfile_source = "%(VERSIONFILE_SOURCE)s" + +def get_versions(default={"version": "unknown", "full": ""}, verbose=False): + variables = { "refnames": git_refnames, "full": git_full } + ver = versions_from_expanded_variables(variables, tag_prefix, verbose) + if not ver: + ver = versions_from_vcs(tag_prefix, versionfile_source, verbose) + if not ver: + ver = versions_from_parentdir(parentdir_prefix, versionfile_source, + verbose) + if not ver: + ver = default + return ver + +''' + + +import subprocess +import sys + +def run_command(args, cwd=None, verbose=False): + try: + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen(args, stdout=subprocess.PIPE, cwd=cwd) + except EnvironmentError: + e = sys.exc_info()[1] + if verbose: + print("unable to run %s" % args[0]) + print(e) + return None + stdout = p.communicate()[0].strip() + if sys.version >= '3': + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % args[0]) + return None + return stdout + + +import sys +import re +import os.path + +def get_expanded_variables(versionfile_source): + # the code embedded in _version.py can just fetch the value of these + # variables. When used from setup.py, we don't want to import + # _version.py, so we do it with a regexp instead. This function is not + # used from _version.py. + variables = {} + try: + for line in open(versionfile_source,"r").readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + variables["full"] = mo.group(1) + except EnvironmentError: + pass + return variables + +def versions_from_expanded_variables(variables, tag_prefix, verbose=False): + refnames = variables["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("variables are unexpanded, not using") + return {} # unexpanded, so not in an unpacked git-archive tarball + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + for ref in list(refs): + if not re.search(r'\d', ref): + if verbose: + print("discarding '%s', no digits" % ref) + refs.discard(ref) + # Assume all version tags have a digit. git's %d expansion + # behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us + # distinguish between branches and tags. By ignoring refnames + # without digits, we filter out many common branch names like + # "release" and "stabilization", as well as "HEAD" and "master". + if verbose: + print("remaining refs: %s" % ",".join(sorted(refs))) + for ref in sorted(refs): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %s" % r) + return { "version": r, + "full": variables["full"].strip() } + # no suitable tags, so we use the full revision id + if verbose: + print("no suitable tags, using full revision id") + return { "version": variables["full"].strip(), + "full": variables["full"].strip() } + +def versions_from_vcs(tag_prefix, versionfile_source, verbose=False): + # this runs 'git' from the root of the source tree. That either means + # someone ran a setup.py command (and this code is in versioneer.py, so + # IN_LONG_VERSION_PY=False, thus the containing directory is the root of + # the source tree), or someone ran a project-specific entry point (and + # this code is in _version.py, so IN_LONG_VERSION_PY=True, thus the + # containing directory is somewhere deeper in the source tree). This only + # gets called if the git-archive 'subst' variables were *not* expanded, + # and _version.py hasn't already been rewritten with a short version + # string, meaning we're inside a checked out source tree. + + try: + here = os.path.abspath(__file__) + except NameError: + # some py2exe/bbfreeze/non-CPython implementations don't do __file__ + return {} # not always correct + + # versionfile_source is the relative path from the top of the source tree + # (where the .git directory might live) to this file. Invert this to find + # the root from __file__. + root = here + if IN_LONG_VERSION_PY: + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + root = os.path.dirname(here) + if not os.path.exists(os.path.join(root, ".git")): + if verbose: + print("no .git in %s" % root) + return {} + + GIT = "git" + if sys.platform == "win32": + GIT = "git.cmd" + stdout = run_command([GIT, "describe", "--tags", "--dirty", "--always"], + cwd=root) + if stdout is None: + return {} + if not stdout.startswith(tag_prefix): + if verbose: + print("tag '%s' doesn't start with prefix '%s'" % (stdout, tag_prefix)) + return {} + tag = stdout[len(tag_prefix):] + stdout = run_command([GIT, "rev-parse", "HEAD"], cwd=root) + if stdout is None: + return {} + full = stdout.strip() + if tag.endswith("-dirty"): + full += "-dirty" + return {"version": tag, "full": full} + + +def versions_from_parentdir(parentdir_prefix, versionfile_source, verbose=False): + if IN_LONG_VERSION_PY: + # We're running from _version.py. If it's from a source tree + # (execute-in-place), we can work upwards to find the root of the + # tree, and then check the parent directory for a version string. If + # it's in an installed application, there's no hope. + try: + here = os.path.abspath(__file__) + except NameError: + # py2exe/bbfreeze/non-CPython don't have __file__ + return {} # without __file__, we have no hope + # versionfile_source is the relative path from the top of the source + # tree to _version.py. Invert this to find the root from __file__. + root = here + for i in range(len(versionfile_source.split("/"))): + root = os.path.dirname(root) + else: + # we're running from versioneer.py, which means we're running from + # the setup.py in a source tree. sys.argv[0] is setup.py in the root. + here = os.path.abspath(sys.argv[0]) + root = os.path.dirname(here) + + # Source tarballs conventionally unpack into a directory that includes + # both the project name and a version string. + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): + if verbose: + print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" % + (root, dirname, parentdir_prefix)) + return None + return {"version": dirname[len(parentdir_prefix):], "full": ""} + +import sys + +def do_vcs_install(versionfile_source, ipy): + GIT = "git" + if sys.platform == "win32": + GIT = "git.cmd" + run_command([GIT, "add", "versioneer.py"]) + run_command([GIT, "add", versionfile_source]) + run_command([GIT, "add", ipy]) + present = False + try: + f = open(".gitattributes", "r") + for line in f.readlines(): + if line.strip().startswith(versionfile_source): + if "export-subst" in line.strip().split()[1:]: + present = True + f.close() + except EnvironmentError: + pass + if not present: + f = open(".gitattributes", "a+") + f.write("%s export-subst\n" % versionfile_source) + f.close() + run_command([GIT, "add", ".gitattributes"]) + + +SHORT_VERSION_PY = """ +# This file was generated by 'versioneer.py' (0.7+) from +# revision-control system data, or from the parent directory name of an +# unpacked source archive. Distribution tarballs contain a pre-generated copy +# of this file. + +version_version = '%(version)s' +version_full = '%(full)s' +def get_versions(default={}, verbose=False): + return {'version': version_version, 'full': version_full} + +""" + +DEFAULT = {"version": "unknown", "full": "unknown"} + +def versions_from_file(filename): + versions = {} + try: + f = open(filename) + except EnvironmentError: + return versions + for line in f.readlines(): + mo = re.match("version_version = '([^']+)'", line) + if mo: + versions["version"] = mo.group(1) + mo = re.match("version_full = '([^']+)'", line) + if mo: + versions["full"] = mo.group(1) + return versions + +def write_to_version_file(filename, versions): + f = open(filename, "w") + f.write(SHORT_VERSION_PY % versions) + f.close() + print("set %s to '%s'" % (filename, versions["version"])) + + +def get_best_versions(versionfile, tag_prefix, parentdir_prefix, + default=DEFAULT, verbose=False): + # returns dict with two keys: 'version' and 'full' + # + # extract version from first of _version.py, 'git describe', parentdir. + # This is meant to work for developers using a source checkout, for users + # of a tarball created by 'setup.py sdist', and for users of a + # tarball/zipball created by 'git archive' or github's download-from-tag + # feature. + + variables = get_expanded_variables(versionfile_source) + if variables: + ver = versions_from_expanded_variables(variables, tag_prefix) + if ver: + if verbose: print("got version from expanded variable %s" % ver) + return ver + + ver = versions_from_file(versionfile) + if ver: + if verbose: print("got version from file %s %s" % (versionfile, ver)) + return ver + + ver = versions_from_vcs(tag_prefix, versionfile_source, verbose) + if ver: + if verbose: print("got version from git %s" % ver) + return ver + + ver = versions_from_parentdir(parentdir_prefix, versionfile_source, verbose) + if ver: + if verbose: print("got version from parentdir %s" % ver) + return ver + + if verbose: print("got version from default %s" % ver) + return default + +def get_versions(default=DEFAULT, verbose=False): + assert versionfile_source is not None, "please set versioneer.versionfile_source" + assert tag_prefix is not None, "please set versioneer.tag_prefix" + assert parentdir_prefix is not None, "please set versioneer.parentdir_prefix" + return get_best_versions(versionfile_source, tag_prefix, parentdir_prefix, + default=default, verbose=verbose) +def get_version(verbose=False): + return get_versions(verbose=verbose)["version"] + +class cmd_version(Command): + description = "report generated version string" + user_options = [] + boolean_options = [] + def initialize_options(self): + pass + def finalize_options(self): + pass + def run(self): + ver = get_version(verbose=True) + print("Version is currently: %s" % ver) + + +class cmd_build(_build): + def run(self): + versions = get_versions(verbose=True) + _build.run(self) + # now locate _version.py in the new build/ directory and replace it + # with an updated value + target_versionfile = os.path.join(self.build_lib, versionfile_build) + print("UPDATING %s" % target_versionfile) + os.unlink(target_versionfile) + f = open(target_versionfile, "w") + f.write(SHORT_VERSION_PY % versions) + f.close() + +class cmd_sdist(_sdist): + def run(self): + versions = get_versions(verbose=True) + self._versioneer_generated_versions = versions + # unless we update this, the command will keep using the old version + self.distribution.metadata.version = versions["version"] + return _sdist.run(self) + + def make_release_tree(self, base_dir, files): + _sdist.make_release_tree(self, base_dir, files) + # now locate _version.py in the new base_dir directory (remembering + # that it may be a hardlink) and replace it with an updated value + target_versionfile = os.path.join(base_dir, versionfile_source) + print("UPDATING %s" % target_versionfile) + os.unlink(target_versionfile) + f = open(target_versionfile, "w") + f.write(SHORT_VERSION_PY % self._versioneer_generated_versions) + f.close() + +INIT_PY_SNIPPET = """ +from ._version import get_versions +__version__ = get_versions()['version'] +del get_versions +""" + +class cmd_update_files(Command): + description = "modify __init__.py and create _version.py" + user_options = [] + boolean_options = [] + def initialize_options(self): + pass + def finalize_options(self): + pass + def run(self): + ipy = os.path.join(os.path.dirname(versionfile_source), "__init__.py") + print(" creating %s" % versionfile_source) + f = open(versionfile_source, "w") + f.write(LONG_VERSION_PY % {"DOLLAR": "$", + "TAG_PREFIX": tag_prefix, + "PARENTDIR_PREFIX": parentdir_prefix, + "VERSIONFILE_SOURCE": versionfile_source, + }) + f.close() + try: + old = open(ipy, "r").read() + except EnvironmentError: + old = "" + if INIT_PY_SNIPPET not in old: + print(" appending to %s" % ipy) + f = open(ipy, "a") + f.write(INIT_PY_SNIPPET) + f.close() + else: + print(" %s unmodified" % ipy) + do_vcs_install(versionfile_source, ipy) + +def get_cmdclass(): + return {'version': cmd_version, + 'update_files': cmd_update_files, + 'build': cmd_build, + 'sdist': cmd_sdist, + } From c305f8c5c383f137223abf2591992fe1a70d1271 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sun, 12 May 2013 10:22:44 +0000 Subject: [PATCH 224/397] Add tests to Makefile cmds, and add better test cleanup. --- Makefile | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 4c562fd..49172ae 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,28 @@ -clean: - rm -f \#*\# - rm -f ./*.pyc - rm -f ./*.pyo +cleanup-src: + cd src && \ + rm -f \#*\# && \ + rm -f ./*.pyc && \ + rm -f ./*.pyo -cleantest: clean - mkdir -p gnupg/tests/tmp_test - touch gnupg/tests/placeholder.log - rm -rf gnupg/tests/tmp_test - rm gnupg/tests/*.log +cleanup-tests: + cd tests && \ + rm -f \#*\# && \ + rm -f ./*.pyc && \ + rm -f ./*.pyo + +cleanup-build: + mkdir buildnot + rm -rf build* + +cleantest: cleanup-src cleanup-tests cleanup-build + mkdir -p tests/tmp + touch placeholder.log + rm -rf tests/tmp + rm *.log test: cleantest - python gnupg/tests/test_gnupg.py parsers basic genkey sign listkeys crypt keyrings import + python tests/test_gnupg.py parsers basic genkey sign listkeys crypt keyrings import install: python setup.py install --record installed-files.txt From 0fa269c93c26c14619a0782cdacf410a5d0cd7c9 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 13 May 2013 00:44:29 +0000 Subject: [PATCH 225/397] Fix the directory shortcuts in utils.py according to the new module layout. --- src/util.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/util.py b/src/util.py index 4ddbc65..40aac99 100644 --- a/src/util.py +++ b/src/util.py @@ -58,9 +58,10 @@ except NameError: _py3k = True ## Directory shortcuts: -_here = os.getcwd() ## .../python-gnupg/gnupg +_here = os.getcwd() ## .../python-gnupg/src _repo = _here.rsplit(__module__, 1)[0] ## .../python-gnupg -_test = os.path.join(_repo, 'tmp_test') ## .../python-gnupg/tmp_test +_test = os.path.join(os.path.join(_repo, 'tests'), + 'tmp') ## .../python-gnupg/tests/tmp _user = os.environ.get('HOME') ## $HOME _ugpg = os.path.join(_user, '.gnupg') ## $HOME/.gnupg _conf = os.path.join(os.path.join(_user, '.config'), From c5a7ccb572e1f9a95ab7a89e369bc80c4c1db9ff Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 13 May 2013 00:46:18 +0000 Subject: [PATCH 226/397] Update module level docstring. --- src/gnupg.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 9d73511..531e8a0 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1,7 +1,7 @@ #!/usr/bin/env python #-*- encoding: utf-8 -*- # -# This file is part of python-gnupg, a Python wrapper around GnuPG. +# This file is part of python-gnupg, a Python interface to GnuPG. # Copyright Ā© 2013 Isis Lovecruft # Ā© 2008-2012 Vinay Sajip # Ā© 2005 Steve Traugott @@ -16,8 +16,7 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. -""" -gnupg.py +"""gnupg.py ======== A Python interface to GnuPG. @@ -26,9 +25,9 @@ Sajip, which itself is a modification of GPG.py written by Steve Traugott, which in turn is a modification of the pycrypto GnuPG interface written by A.M. Kuchling. -This version is patched to exclude calls to :class:`subprocess.Popen([...], -shell=True)`, and it also attempts to provide sanitization of arguments -presented to gnupg, in order to avoid potential vulnerabilities. +This version is patched to sanitize untrusted inputs, due to the necessity of +executing :class:`subprocess.Popen([...], shell=True)` in order to communicate +with GnuPG. :Info: see :Authors: A.M. Kuchling, Steve Traugott, Vinay Sajip, Isis Lovecruft @@ -72,7 +71,6 @@ Vinay Sajip's documentation: A unittest harness (test_gnupg.py) has also been added. Modifications Copyright (C) 2008-2012 Vinay Sajip. All rights reserved. - """ try: From e462cf42edff51bde6bf8588219a8b9f64e9d0d4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 13 May 2013 00:53:41 +0000 Subject: [PATCH 227/397] Call parsers._check_preferences() to set default_preference_list. --- src/gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 531e8a0..1f6b7ea 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -98,6 +98,7 @@ import threading from parsers import Verify, Crypt, DeleteResult, ImportResult from parsers import GenKey, Sign, ListKeys, ListPackets from parsers import _fix_unsafe, _sanitise, _is_allowed, _sanitise_list +from parsers import _check_preferences from util import logger, _conf import util as _util @@ -157,8 +158,7 @@ class GPG(object): if default_preference_list is None: prefs = 'SHA512 SHA384 SHA256 AES256 CAMELLIA256 TWOFISH ZLIB ZIP' else: - ## xxx implement me, should return None on error - prefs = check_preference_list(default_preference_list) + prefs = _check_preferences(default_preference_list) self.default_preference_list = prefs secring = 'secring.gpg' if secring is None else _fix_unsafe(secring) From 1588822f0268c9ce8ee91e654119a3fe55b0b8ca Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 13 May 2013 09:14:42 +0000 Subject: [PATCH 228/397] Fix accidental misnaming from sed 's/pubring/keyring/'. --- tests/test_gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 53f5b51..7eb7c9f 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -135,7 +135,7 @@ class GPGTestCase(unittest.TestCase): shutil.rmtree(hd) self.homedir = hd self.gpg = gnupg.GPG(homedir=hd, binary='gpg') - self.keyring = os.path.join(self.homedir, 'keyring.gpg') + self.keyring = os.path.join(self.homedir, 'pubring.gpg') self.secring = os.path.join(self.homedir, 'secring.gpg') def tearDown(self): From a9154b25d55a451925d2604e29980ac2da71fcf0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 13 May 2013 10:16:39 +0000 Subject: [PATCH 229/397] Add .gitattributes file for versioneer. --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..26e3729 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +src/_version.py export-subst From 26da5c3e731291092484896e13a4e1e5162428cf Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 00:39:00 +0000 Subject: [PATCH 230/397] Remove copyright info from top level of module. * Previously, it was: In [1]: import gnupg In [2]: gnupg. gnupg.GPG gnupg.copyright gnupg.logger gnupg.util gnupg.copyleft gnupg.gnupg gnupg.parsers So I've started cleaning up the API so the only thing visible on module import is the class you probably need: gnupg.GPG . --- src/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 92998b8..0294ac0 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,11 +1,9 @@ -#-*- encoding: utf-8 -*- - -from .copyleft import disclaimer as copyright -from .copyleft import txcopyright import gnupg from parsers import Crypt, DeleteResult, ListKeys from parsers import GenKey, Sign, ImportResult, Verify +import copyleft + from gnupg import GPG from ._version import get_versions @@ -16,8 +14,9 @@ gnupg.__version__ = __version__ gnupg.__author__ = 'Isis Agora Lovecruft' gnupg.__contact__ = 'isis@leap.se' gnupg.__url__ = 'https://github.com/isislovecruft/python-gnupg' -gnupg.__license__ = copyright +gnupg.__license__ = copyleft.disclaimer __all__ = ["gnupg", "copyright", "Crypt", "DeleteResult", "ListKeys", "GenKey", "Sign", "Encrypt", "ImportResult", "Verify"] +del copyleft From 41d9ab27bd06f0f51e31a2d3b34025a643a3437f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 00:50:32 +0000 Subject: [PATCH 231/397] Don't point users to github, where possible. * Github has these rather nasty tracking mechanisms through pixel cookies, as well as hardware fingerprinting (potentially deanonymising) through HTML5 canvases. * I suck at configuring gitweb + lighttpd. I cannot get pretty URLs for the repos, but it's still better than the pixel cookies. If anyone can point me at something for fixing this, much obliged. I wasn't sure if I should point explicitly to a LEAP server. --- src/gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index 1f6b7ea..0b4a29e 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -85,7 +85,7 @@ from subprocess import PIPE import codecs ## For AOS, the locale module will need to point to a wrapper around the ## java.util.Locale class. -## See https://github.com/isislovecruft/android-locale-hack +## See https://code.patternsinthevoid.net/?p=android-locale-hack.git import locale import logging import os From 81514e0ec47c34259890f7eb7806143311921bd4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 00:58:43 +0000 Subject: [PATCH 232/397] Fix parsers._check_preferences function to use set theoretic intersection. * It benchmarks super fast. And I get to use set theory. --- src/parsers.py | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/parsers.py b/src/parsers.py index 4477708..840844c 100644 --- a/src/parsers.py +++ b/src/parsers.py @@ -44,31 +44,40 @@ class UsageError(Exception): def _check_preferences(prefs, pref_type=None): - cipher = frozenset(['AES256', 'AES192', 'CAMELLIA256', 'CAMELLIA192', - 'TWOFISH',]) - digest = frozenset(['SHA512', 'SHA384', 'SHA256', 'SHA224']) - compress = frozenset(['ZLIB', 'ZIP', 'Uncompressed']) - all = frozenset([ciphers, hashes, compress]) + if prefs is None: return + + cipher = frozenset(['AES256', 'AES192', 'AES128', + 'CAMELLIA256', 'CAMELLIA192', + 'TWOFISH', '3DES']) + digest = frozenset(['SHA512', 'SHA384', 'SHA256', 'SHA224', 'RMD160', + 'SHA1']) + compress = frozenset(['BZIP2', 'ZLIB', 'ZIP', 'Uncompressed']) + all = frozenset([cipher, digest, compress]) if isinstance(prefs, str): - prefs = prefs.split(' ') - if not pref_type: - pref_type = all + prefs = set(prefs.split()) + elif isinstance(prefs, list): + prefs = set(prefs) + else: + msg = "prefs must be a list of strings, or one space-separated string" + log.error("parsers._check_preferences(): %s" % message) + raise TypeError(message) + + if not pref_type: + pref_type = 'all' + + allowed = str() - ## xxx we should use set differences if pref_type == 'cipher': - for pref in prefs: - if not cipher.contains(pref): return + allowed += ' '.join(prefs.intersection(cipher)) if pref_type == 'digest': - for pref in prefs: - if not digest.contains(pref): return + allowed += ' '.join(prefs.intersection(digest)) if pref_type == 'compress': - for pref in prefs: - if not compress.contains(pref): return + allowed += ' '.join(prefs.intersection(compress)) if pref_type == 'all': - for pref in prefs: - if not all.contains(pref): return - return ' '.join([pref for pref in prefs]) + allowed += ' '.join(prefs.intersection(all)) + + return allowed def _fix_unsafe(shell_input): """Find characters used to escape from a string into a shell, and wrap them From 615e2cfa600e7ae43922f95b68597e4ab31719b8 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 01:00:05 +0000 Subject: [PATCH 233/397] Add docstring for parsers._check_preferences. --- src/parsers.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/parsers.py b/src/parsers.py index 840844c..aebbd97 100644 --- a/src/parsers.py +++ b/src/parsers.py @@ -44,6 +44,13 @@ class UsageError(Exception): def _check_preferences(prefs, pref_type=None): + """Check cipher, digest, and compression preference settings. + + MD5 is not allowed. This is not 1994.[0] SHA1 is allowed grudgingly.[1] + + [0]: http://www.cs.colorado.edu/~jrblack/papers/md5e-full.pdf + [1]: http://eprint.iacr.org/2008/469.pdf + """ if prefs is None: return cipher = frozenset(['AES256', 'AES192', 'AES128', From b84268fecd85d86ee6bdce7dfc7a5bbd8f878b76 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 01:02:27 +0000 Subject: [PATCH 234/397] Locally scoped ivars in _check_preferences don't need private namespace. --- src/parsers.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/parsers.py b/src/parsers.py index aebbd97..fba0062 100644 --- a/src/parsers.py +++ b/src/parsers.py @@ -140,7 +140,7 @@ def _is_allowed(input): if no errors occur. """ - _all = (""" + three_hundred_eighteen = (""" --allow-freeform-uid --multifile --allow-multiple-messages --no --allow-multisig-verification --no-allow-freeform-uid @@ -302,7 +302,7 @@ def _is_allowed(input): --min-cert-level --yes """).split() - _possible = frozenset(_all) + possible = frozenset(three_hundred_eighteen) ## these are the allowed options we will handle so far, all others should ## be dropped. this dance is so that when new options are added later, we @@ -342,9 +342,9 @@ def _is_allowed(input): ## check that _allowed is a subset of _possible try: - assert _allowed.issubset(_possible), \ - '_allowed is not subset of known options, difference: %s' \ - % _allowed.difference(_possible) + assert _allowed.issubset(possible), \ + 'allowed is not subset of known options, difference: %s' \ + % _allowed.difference(possible) except AssertionError as ae: logger.debug("_is_allowed(): %s" % ae.message) raise UsageError(ae.message) From 2686131809f601f2402b71eeba1466e63cde28c1 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 01:04:45 +0000 Subject: [PATCH 235/397] Reorder frozenset of allowed options so that it's easier to read. --- src/parsers.py | 111 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 35 deletions(-) diff --git a/src/parsers.py b/src/parsers.py index fba0062..113cdbc 100644 --- a/src/parsers.py +++ b/src/parsers.py @@ -312,39 +312,81 @@ def _is_allowed(input): ## xxx checkout the --store option for creating rfc1991 data packets ## xxx key fetching/retrieving options: [fetch_keys, merge_only, recv_keys] ## - _allowed = frozenset( - ['--list-keys', '--list-key', '--fixed-list-mode', - '--list-secret-keys', '--list-public-keys', - '--list-packets', '--with-colons', - '--list-options', - '--delete-keys', '--delete-secret-keys', - '--encrypt', '--encrypt-files', - '--decrypt', '--decrypt-files', - '--always-trust', - '--symmetric', - '--use-agent', '--no-use-agent', - '--print-mds', '--print-md', - '--sign', '--clearsign', '--detach-sign', - '--armor', '--armour', - '--gen-key', '--batch', - '--decrypt', '--decrypt-files', '--multifile', '--output', - '--cert-digest-algo', '--digest-algo', - '--cipher-algo', '--compress-algo', '--personal-digest-prefs', - '--personal-cipher-prefs', '--personal-compress-prefs', - '--import', - '--export', '--export-secret-keys', '--export-secret-subkeys', - '--verify', - '--version', '--no-emit-version', '--output', - '--status-fd', '--no-tty', '--passphrase-fd', - '--homedir', '--no-default-keyring', '--default-key', - '--keyring', '--secret-keyring', '--primary-keyring', - '--fingerprint',]) + allowed = frozenset(['--fixed-list-mode', ## key/packet listing + '--list-key', + '--list-keys', + '--list-options', + '--list-packets', + '--list-public-keys', + '--list-secret-keys', + '--print-md', + '--print-mds', + '--with-colons', + ## deletion + '--delete-keys', + '--delete-secret-keys', + ## en-/de-cryption + '--always-trust', + '--decrypt', + '--decrypt-files', + '--encrypt', + '--encrypt-files', + '--recipient', + '--no-default-recipient', + '--symmetric', + '--use-agent', + '--no-use-agent', + ## signing/certification + '--armor', + '--armour', + '--clearsign', + '--detach-sign', + '--sign', + '--verify', + ## i/o and files + '--batch', + '--debug-all', + '--debug-level', + '--gen-key', + #'--multifile', + '--no-emit-version', + '--no-tty', + '--output', + '--passphrase-fd', + '--status-fd', + '--version', + ## keyring, homedir, & options + '--homedir', + '--keyring', + '--primary-keyring', + '--secret-keyring', + '--no-default-keyring', + '--default-key', + '--no-options', + ## preferences + '--digest-algo', + '--cipher-algo', + '--compress-algo', + '--compression-algo', + '--cert-digest-algo', + '--personal-digest-prefs', + '--personal-digest-preferences', + '--personal-cipher-prefs', + '--personal-cipher-preferences', + '--personal-compress-prefs', + '--personal-compress-preferences', + ## export/import + '--import', + '--export', + '--export-secret-keys', + '--export-secret-subkeys', + '--fingerprint',]) - ## check that _allowed is a subset of _possible + ## check that allowed is a subset of possible try: - assert _allowed.issubset(possible), \ + assert allowed.issubset(possible), \ 'allowed is not subset of known options, difference: %s' \ - % _allowed.difference(possible) + % allowed.difference(possible) except AssertionError as ae: logger.debug("_is_allowed(): %s" % ae.message) raise UsageError(ae.message) @@ -362,12 +404,11 @@ def _is_allowed(input): else: hyphenated = input try: - assert hyphenated in _allowed + assert hyphenated in allowed except AssertionError as ae: - logger.warn("_is_allowed(): Dropping option '%s'..." - % _fix_unsafe(hyphenated)) - raise ProtectedOption("Option '%s' not supported." - % _fix_unsafe(hyphenated)) + dropped = _fix_unsafe(hyphenated) + logger.warn("_is_allowed(): Dropping option '%s'..." % dropped) + raise ProtectedOption("Option '%s' not supported." % dropped) else: return input return None From 5fe8320fd0e3f30573ee8df500de6249ea505deb Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 01:08:07 +0000 Subject: [PATCH 236/397] We don't need module strings with a flat directory structure. --- src/parsers.py | 3 --- src/util.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/parsers.py b/src/parsers.py index 113cdbc..19fe8c3 100644 --- a/src/parsers.py +++ b/src/parsers.py @@ -22,9 +22,6 @@ parsers.py Classes for parsing GnuPG status messages and sanitising commandline options. ''' -__module__ = 'gnupg.parsers' - - import logging import re diff --git a/src/util.py b/src/util.py index 40aac99..262c755 100644 --- a/src/util.py +++ b/src/util.py @@ -22,9 +22,6 @@ utils.py Extra utilities for python-gnupg. ''' -__module__ = 'gnupg.util' - - from datetime import datetime import logging From 3038e3948be8a2b68f32b9f7ea2928c1fbfde6a0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 01:13:02 +0000 Subject: [PATCH 237/397] Remove relative imports of every class in parsers.py, they crowd the namespace. --- src/__init__.py | 2 -- src/gnupg.py | 22 +++++++++++----------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 0294ac0..b565e07 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,7 +1,5 @@ import gnupg -from parsers import Crypt, DeleteResult, ListKeys -from parsers import GenKey, Sign, ImportResult, Verify import copyleft from gnupg import GPG diff --git a/src/gnupg.py b/src/gnupg.py index 0b4a29e..1495537 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -95,12 +95,11 @@ import sys import tempfile import threading -from parsers import Verify, Crypt, DeleteResult, ImportResult -from parsers import GenKey, Sign, ListKeys, ListPackets -from parsers import _fix_unsafe, _sanitise, _is_allowed, _sanitise_list -from parsers import _check_preferences from util import logger, _conf +import parsers + + import util as _util @@ -108,13 +107,14 @@ class GPG(object): """Encapsulate access to the gpg executable""" _decode_errors = 'strict' - _result_map = {'crypt': Crypt, - 'delete': DeleteResult, - 'generate': GenKey, - 'import': ImportResult, - 'list': ListKeys, - 'sign': Sign, - 'verify': Verify,} + _result_map = { 'crypt': parsers.Crypt, + 'delete': parsers.DeleteResult, + 'generate': parsers.GenKey, + 'import': parsers.ImportResult, + 'list': parsers.ListKeys, + 'sign': parsers.Sign, + 'verify': parsers.Verify, + 'packets': parsers.ListPackets } def __init__(self, binary=None, homedir=None, verbose=False, use_agent=False, keyring=None, secring=None, From 891ee9e7ebc9f2a67b73c8ebe69c426dddb2bbc9 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 02:15:17 +0000 Subject: [PATCH 238/397] Significantly improve logging facilities for faster debugging. * Include _ansistrm.py, which was released under new BSD license separately in a github gist as well as in the Python logutils module. Instead of setting up a src/includes/ directory, I have included this file in the currently flat module directory, because dealing with submodule imports in Python2.x is a pain. I'm not sure if this will cause problems for Debian packaging, but it could be changed later if so. The full text of the original license and copyright has been retained at the head of _ansistrm.py, clearly demarcated from python-gnupg's license and copyright info. The author of _ansistrm.py is the same as the current upstream maintainer for python-gnupg, Vinay Sajip, and I don't see why this person doesn't include _ansistrm.py in all of their packages, because it is making skimming the test logs for errors much faster. * Add _logger.py, which contains a class decorator: @wraps(logging.Logger) def create_logger(level=logging.NOTSET) [...] which autoconfigures logging functionality. By default, logging.NullHandler is still used, so there are no logs (neither logfiles nor piped to stdout). If _logger.create_logger() is called with either a LEVEL attribute from the logging module, or the integer equivalent, it will initialise logging to stdout only. In the unittest suite, extra log handling functionality is added to also write to a datetime-stamped logfile in the tests/ directory. * Change all "logger." statements to "log." and add new ones for debugging and user information purposes. * Remove the import of the python standard utilities logging module from: src/gnupg.py src/parsers.py src/util.py and switch to setting a top-level attribute, util.log, which is the returned class from _logger.create_logger(). Everything else now does "from util import log". --- src/_ansistrm.py | 171 ++++++++++++++++++++++++++++++++++++++++++++ src/_logger.py | 65 +++++++++++++++++ src/gnupg.py | 81 +++++++++++---------- src/parsers.py | 50 ++++++------- src/util.py | 45 ++++++------ tests/test_gnupg.py | 57 ++++++++------- 6 files changed, 357 insertions(+), 112 deletions(-) create mode 100644 src/_ansistrm.py create mode 100644 src/_logger.py diff --git a/src/_ansistrm.py b/src/_ansistrm.py new file mode 100644 index 0000000..b4c8020 --- /dev/null +++ b/src/_ansistrm.py @@ -0,0 +1,171 @@ +# +# This file is part of python-gnupg, a Python wrapper aroung GnuPG, and it was +# taken from https://gist.github.com/vsajip/758430 on the 14th of May, 2013. It +# has also been included in the 'logutils' Python module, see +# https://code.google.com/p/logutils/ . +# +# The original copyright and license text are as follows: +# | +# | Copyright (C) 2010-2012 Vinay Sajip. All rights reserved. +# | Licensed under the new BSD license. +# | +# +# This file is part of python-gnupg, a Python wrapper around GnuPG. +# Copyright Ā© 2013 Isis Lovecruft, Andrej B. +# Ā© 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 Affero 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 +# GNU Affero General Public License for more details. + + +import ctypes +import logging +import os + +class ColorizingStreamHandler(logging.StreamHandler): + # color names to indices + color_map = { + 'black': 0, + 'red': 1, + 'green': 2, + 'yellow': 3, + 'blue': 4, + 'magenta': 5, + 'cyan': 6, + 'white': 7, + } + + #levels to (background, foreground, bold/intense) + if os.name == 'nt': + level_map = { + logging.DEBUG: (None, 'blue', True), + logging.INFO: (None, 'green', False), + logging.WARNING: (None, 'yellow', True), + logging.ERROR: (None, 'red', True), + logging.CRITICAL: ('red', 'white', True), + } + else: + level_map = { + logging.DEBUG: (None, 'blue', False), + logging.INFO: (None, 'green', False), + logging.WARNING: (None, 'yellow', False), + logging.ERROR: (None, 'red', False), + logging.CRITICAL: ('red', 'white', True), + } + csi = '\x1b[' + reset = '\x1b[0m' + + @property + def is_tty(self): + isatty = getattr(self.stream, 'isatty', None) + return isatty and isatty() + + def emit(self, record): + try: + message = self.format(record) + stream = self.stream + if not self.is_tty: + stream.write(message) + else: + self.output_colorized(message) + stream.write(getattr(self, 'terminator', '\n')) + self.flush() + except (KeyboardInterrupt, SystemExit): + raise + except: + self.handleError(record) + + if os.name != 'nt': + def output_colorized(self, message): + self.stream.write(message) + else: + import re + ansi_esc = re.compile(r'\x1b\[((?:\d+)(?:;(?:\d+))*)m') + + nt_color_map = { + 0: 0x00, # black + 1: 0x04, # red + 2: 0x02, # green + 3: 0x06, # yellow + 4: 0x01, # blue + 5: 0x05, # magenta + 6: 0x03, # cyan + 7: 0x07, # white + } + + def output_colorized(self, message): + parts = self.ansi_esc.split(message) + write = self.stream.write + h = None + fd = getattr(self.stream, 'fileno', None) + if fd is not None: + fd = fd() + if fd in (1, 2): # stdout or stderr + h = ctypes.windll.kernel32.GetStdHandle(-10 - fd) + while parts: + text = parts.pop(0) + if text: + write(text) + if parts: + params = parts.pop(0) + if h is not None: + params = [int(p) for p in params.split(';')] + color = 0 + for p in params: + if 40 <= p <= 47: + color |= self.nt_color_map[p - 40] << 4 + elif 30 <= p <= 37: + color |= self.nt_color_map[p - 30] + elif p == 1: + color |= 0x08 # foreground intensity on + elif p == 0: # reset to default color + color = 0x07 + else: + pass # error condition ignored + ctypes.windll.kernel32.SetConsoleTextAttribute(h, color) + + def colorize(self, message, record): + if record.levelno in self.level_map: + bg, fg, bold = self.level_map[record.levelno] + params = [] + if bg in self.color_map: + params.append(str(self.color_map[bg] + 40)) + if fg in self.color_map: + params.append(str(self.color_map[fg] + 30)) + if bold: + params.append('1') + if params: + message = ''.join((self.csi, ';'.join(params), + 'm', message, self.reset)) + return message + + def format(self, record): + message = logging.StreamHandler.format(self, record) + if self.is_tty: + # Don't colorize any traceback + parts = message.split('\n', 1) + parts[0] = self.colorize(parts[0], record) + message = '\n'.join(parts) + return message + +def main(): + root = logging.getLogger() + root.setLevel(logging.DEBUG) + root.addHandler(ColorizingStreamHandler()) + logging.debug('DEBUG') + logging.info('INFO') + logging.warning('WARNING') + logging.error('ERROR') + logging.critical('CRITICAL') + +if __name__ == '__main__': + main() diff --git a/src/_logger.py b/src/_logger.py new file mode 100644 index 0000000..eddf9f2 --- /dev/null +++ b/src/_logger.py @@ -0,0 +1,65 @@ +#-*- encoding: utf-8 -*- +# +# This file is part of python-gnupg, a Python wrapper around GnuPG. +# Copyright Ā© 2013 Isis Lovecruft, Andrej B. +# Ā© 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 Affero 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 +# GNU Affero General Public License for more details. +'''log.py +---------- +Logging module for python-gnupg. +''' +__name__ = '_logger' + +from functools import wraps + +import ctypes +import logging +import os +import sys + +try: + from logging import NullHandler +except: + class NullHandler(logging.Handler): + def handle(self, record): + pass + +import gnupg._ansistrm + +#log = logging.getLogger('gnupg') +#if not log.handlers: +# log.addHandler(NullHandler()) + + +@wraps(logging.Logger) +def create_logger(level=logging.NOTSET): + """Create a logger for python-gnupg at a specific message level.""" + + log = logging.getLogger('gnupg') + + if level > logging.NOTSET: + logging.captureWarnings(True) + logging.logThreads = True + log.setLevel(level) + + colorizer = gnupg._ansistrm.ColorizingStreamHandler(stream=sys.stdout) + colorizer.setLevel(level) + log.addHandler(colorizer) + + log.debug("Starting the logger...") + + if not log.handlers: + log.addHandler(NullHandler()) + + return log diff --git a/src/gnupg.py b/src/gnupg.py index 1495537..1f59b14 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -95,7 +95,7 @@ import sys import tempfile import threading -from util import logger, _conf +from util import log, _conf import parsers @@ -151,7 +151,7 @@ class GPG(object): _util._create_homedir(self.homedir) else: message = ("Unsuitable gpg home dir: %s" % homedir) - logger.debug("GPG.__init__(): %s" % message) + log.debug("GPG.__init__(): %s" % message) self.binary = _util._find_binary(binary) @@ -183,7 +183,7 @@ class GPG(object): assert isinstance(options, str), ("options not formatted: %s" % options) except (AssertionError, AttributeError) as ae: - logger.debug("GPG.__init__(): %s" % ae.message) + log.debug("GPG.__init__(): %s" % ae.message) raise RuntimeError(ae.message) else: self.verbose = verbose @@ -226,9 +226,7 @@ class GPG(object): communicating with it. """ cmd = ' '.join(self._make_args(args, passphrase)) - if self.verbose: - print(cmd) - logger.debug("_open_subprocess(): %s", cmd) + log.debug("_open_subprocess(): %s", cmd) return Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE) def _read_response(self, stream, result): @@ -246,8 +244,9 @@ class GPG(object): lines.append(line) line = line.rstrip() if self.verbose: - print(line) - logger.debug("%s", line) + log.info("%s" % line) + else: + log.debug("%s" % line) if line[0:9] == '[GNUPG:] ': # Chop off the prefix line = line[9:] @@ -267,7 +266,7 @@ class GPG(object): data = stream.read(1024) if len(data) == 0: break - logger.debug("chunk: %r" % data[:256]) + log.debug("GPG._read_data(): read chunk: %r" % data[:256]) chunks.append(data) if _util._py3k: # Join using b'' or '', as appropriate @@ -284,13 +283,13 @@ class GPG(object): stderr = codecs.getreader(self.encoding)(process.stderr) rr = threading.Thread(target=self._read_response, args=(stderr, result)) rr.setDaemon(True) - logger.debug('stderr reader: %r', rr) + log.debug('GPG._collect_output(): stderr reader: %r', rr) rr.start() stdout = process.stdout dr = threading.Thread(target=self._read_data, args=(stdout, result)) dr.setDaemon(True) - logger.debug('stdout reader: %r', dr) + log.debug('GPG._collect_output(): stdout reader: %r', dr) dr.start() dr.join() @@ -319,37 +318,46 @@ class GPG(object): self._collect_output(p, result, writer, stdin) return result - # - # SIGNATURE METHODS - # def sign(self, message, **kwargs): """Create a signature for a message or file.""" if isinstance(message, file): + if 'keyid' in kwargs.items(): + log.info("Signing file '%r' with keyid: %s" + % (data, kwargs[keyid])) + else: + log.warn("No 'sign_with' keyid given! Using default key.") result = self._sign_file(message, **kwargs) elif not _util._is_stream(message): + if 'keyid' in kwargs.items(): + log.info("Signing data string '%s' with keyid: %s" + % (data, kwargs[keyid])) + else: + log.warn("No 'sign_with' keyid given! Using default key.") f = _util._make_binary_stream(message, self.encoding) result = self._sign_file(f, **kwargs) f.close() else: - logger.error("Unable to sign message '%s' with type %s" - % (message, type(message))) + log.warn("Unable to sign message '%s' with type %s" + % (data, type(data))) result = None return result def _sign_file(self, file, keyid=None, passphrase=None, clearsign=True, detach=False, binary=False): """Create a signature for a file.""" - logger.debug("_sign_file(): %s", file) + log.debug("_sign_file():") if binary: + log.info("Creating binary signature for file %s" % file) args = ['--sign'] else: + log.info("Creating ascii-armoured signature for file %s" % file) args = ['--sign --armor'] if clearsign: args.append("--clearsign") if detach: - logger.warn("Cannot use both --clearsign and --detach-sign.") - logger.warn("Using default GPG behaviour: --clearsign only.") + log.warn("Cannot use both --clearsign and --detach-sign.") + log.warn("Using default GPG behaviour: --clearsign only.") elif detach and not clearsign: args.append("--detach-sign") @@ -366,7 +374,7 @@ class GPG(object): _util._write_passphrase(stdin, passphrase, self.encoding) writer = _util._threaded_copy_data(file, stdin) except IOError: - logger.exception("_sign_file(): Error writing message") + log.exception("_sign_file(): Error writing message") writer = None self._collect_output(p, result, writer, stdin) return result @@ -408,16 +416,16 @@ class GPG(object): result = self._result_map['verify'](self) if sig_file is None: - logger.debug("verify_file(): Handling embedded signature") + log.debug("verify_file(): Handling embedded signature") args = ["--verify"] proc = self._open_subprocess(args) writer = _util._threaded_copy_data(file, proc.stdin) self._collect_output(proc, result, writer, stdin=proc.stdin) else: if not _util._is_file(sig_file): - logger.debug("verify_file(): '%r' is not a file" % sig_file) + log.debug("verify_file(): '%r' is not a file" % sig_file) return result - logger.debug('verify_file(): Handling detached verification') + log.debug('verify_file(): Handling detached verification') sig_fh = None try: sig_fh = open(sig_file) @@ -430,9 +438,6 @@ class GPG(object): sig_fh.close() return result - # - # KEY MANAGEMENT - # def import_keys(self, key_data): """ Import the key_data into our keyring. @@ -482,10 +487,10 @@ class GPG(object): ## it might be possible to use --list-packets and parse the output result = self._result_map['import'](self) - logger.debug('import_keys: %r', key_data[:256]) + log.debug('import_keys: %r', key_data[:256]) data = _util._make_binary_stream(key_data, self.encoding) self._handle_io(['--import'], data, result, binary=True) - logger.debug('import_keys result: %r', result.__dict__) + log.debug('import_keys result: %r', result.__dict__) data.close() return result @@ -509,12 +514,12 @@ class GPG(object): if keyids is not None: safe_keyids = ' '.join( [(lambda: _fix_unsafe(k))() for k in keyids]) - logger.debug('recv_keys: %r', safe_keyids) + log.debug('recv_keys: %r', safe_keyids) args.extend(safe_keyids) self._handle_io(args, data, result, binary=True) data.close() - logger.debug('recv_keys result: %r', result.__dict__) + log.debug('recv_keys result: %r', result.__dict__) return result def delete_keys(self, fingerprints, secret=False, subkeys=False): @@ -580,7 +585,7 @@ class GPG(object): #stdout, stderr = p.communicate() result = self._result_map['delete'](self) # any result will do self._collect_output(p, result, stdin=p.stdin) - logger.debug('export_keys result: %r', result.data) + log.debug('export_keys result: %r', result.data) return result.data.decode(self.encoding, self._decode_errors) def list_keys(self, secret=False): @@ -619,7 +624,7 @@ class GPG(object): for line in lines: if self.verbose: print(line) - logger.debug("line: %r", line.rstrip()) + log.debug("line: %r", line.rstrip()) if not line: break L = line.strip().split(':') @@ -939,7 +944,7 @@ class GPG(object): result = self._result_map['crypt'](self) self._handle_io(args, file, result, passphrase=passphrase, binary=True) - logger.debug('encrypt result: %r', result.data) + log.debug('GPG.encrypt(): Result: %r', result.data) return result def encrypt(self, data, recipients, **kwargs): @@ -1017,7 +1022,7 @@ class GPG(object): args.append("--always-trust") result = self._result_map['crypt'](self) self._handle_io(args, file, result, passphrase, binary=True) - logger.debug('decrypt result: %r', result.data) + log.debug('decrypt result: %r', result.data) return result @@ -1087,12 +1092,12 @@ class GPGWrapper(GPG): def send_keys(self, keyserver, *keyids): """Send keys to a keyserver.""" result = self._result_map['list'](self) - gnupg.logger.debug('send_keys: %r', keyids) - data = gnupg._util._make_binary_stream("", self.encoding) + log.debug('send_keys: %r', keyids) + data = _util._make_binary_stream("", self.encoding) args = ['--keyserver', keyserver, '--send-keys'] args.extend(keyids) self._handle_io(args, data, result, binary=True) - gnupg.logger.debug('send_keys result: %r', result.__dict__) + log.debug('send_keys result: %r', result.__dict__) data.close() return result @@ -1124,7 +1129,7 @@ class GPGWrapper(GPG): args.append("--always-trust") result = self._result_map['crypt'](self) self._handle_io(args, file, result, passphrase=passphrase, binary=True) - logger.debug('encrypt result: %r', result.data) + log.debug('encrypt result: %r', result.data) return result def list_packets(self, raw_data): diff --git a/src/parsers.py b/src/parsers.py index 19fe8c3..83006c6 100644 --- a/src/parsers.py +++ b/src/parsers.py @@ -22,11 +22,10 @@ parsers.py Classes for parsing GnuPG status messages and sanitising commandline options. ''' -import logging import re -from util import logger import util +from util import log ESCAPE_PATTERN = re.compile(r'\\x([0-9a-f][0-9a-f])', re.I) @@ -385,7 +384,7 @@ def _is_allowed(input): 'allowed is not subset of known options, difference: %s' \ % allowed.difference(possible) except AssertionError as ae: - logger.debug("_is_allowed(): %s" % ae.message) + log.error("_is_allowed(): %s" % ae.message) raise UsageError(ae.message) ## if we got a list of args, join them @@ -404,7 +403,7 @@ def _is_allowed(input): assert hyphenated in allowed except AssertionError as ae: dropped = _fix_unsafe(hyphenated) - logger.warn("_is_allowed(): Dropping option '%s'..." % dropped) + log.warn("_is_allowed(): Dropping option '%s'..." % dropped) raise ProtectedOption("Option '%s' not supported." % dropped) else: return input @@ -471,7 +470,7 @@ def _sanitise(*args): flag = _is_allowed(arg) assert flag is not None, "_check_option(): got None for flag" except (AssertionError, ProtectedOption) as error: - logger.warn("_check_option(): %s" % error.message) + log.warn("_check_option(): %s" % error.message) else: safe_option += (flag + " ") if isinstance(value, str): @@ -485,8 +484,8 @@ def _sanitise(*args): if util._is_file(val): safe_option += (val + " ") else: - logger.debug("_check_option(): %s not file: %s" - % (flag, val)) + log.debug("_check_option(): %s not file: %s" + % (flag, val)) elif flag in ['--default-key', '--recipient', '--export', '--export-secret-keys', '--delete-keys', @@ -494,12 +493,12 @@ def _sanitise(*args): if _is_hex(val): safe_option += (val + " ") else: - logger.debug("_check_option(): '%s %s' not hex." - % (flag, val)) + log.debug("_check_option(): '%s %s' not hex." + % (flag, val)) else: safe_option += (val + " ") - logger.debug("_check_option(): No checks for %s" - % val) + log.debug("_check_option(): No checks for %s" + % val) return safe_option is_flag = lambda x: x.startswith('--') @@ -507,7 +506,7 @@ def _sanitise(*args): def _make_filo(args_string): filo = arg.split(' ') filo.reverse() - logger.debug("_make_filo(): Converted to reverse list: %s" % filo) + log.debug("_make_filo(): Converted to reverse list: %s" % filo) return filo def _make_groups(filo): @@ -515,7 +514,7 @@ def _sanitise(*args): while len(filo) >= 1: last = filo.pop() if is_flag(last): - logger.debug("_make_groups(): Got arg: %s" % last) + log.debug("_make_groups(): Got arg: %s" % last) if last == '--verify': groups[last] = str(filo.pop()) ## accept the read-from-stdin arg: @@ -524,54 +523,55 @@ def _sanitise(*args): else: groups[last] = str() while len(filo) > 1 and not is_flag(filo[len(filo)-1]): - logger.debug("_make_groups(): Got value: %s" + log.debug("_make_groups(): Got value: %s" % filo[len(filo)-1]) groups[last] += (filo.pop() + " ") else: if len(filo) == 1 and not is_flag(filo[0]): - logger.debug("_make_groups(): Got value: %s" % filo[0]) + log.debug("_make_groups(): Got value: %s" % filo[0]) groups[last] += filo.pop() else: - logger.debug("_make_groups(): Got solitary value: %s" % last) + log.warn("_make_groups(): Got solitary value: %s" % last) groups["xxx"] = last return groups def _check_groups(groups): - logger.debug("_check_groups(): Got groups: %s" % groups) + log.debug("_check_groups(): Got groups: %s" % groups) checked_groups = [] for a,v in groups.items(): v = None if len(v) == 0 else v safe = _check_option(a, v) if safe is not None and not safe.strip() == "": - logger.debug("_check_groups(): appending option: %s" % safe) + log.debug("_check_groups(): appending option: %s" % safe) checked_groups.append(safe) else: - logger.debug("_check_groups(): dropped option '%s %s'" % (a,v)) + log.warn("_check_groups(): dropped option '%s %s'" % (a,v)) return checked_groups if args is not None: option_groups = {} for arg in args: - ## if we're given a string with a bunch of options in it split them - ## up and deal with them separately + ## if we're given a string with a bunch of options in it split + ## them up and deal with them separately if isinstance(arg, str): - logger.debug("_sanitise(): Got arg string: %s" % arg) + log.debug("_sanitise(): Got arg string: %s" % arg) if arg.find(' ') > 0: filo = _make_filo(arg) option_groups.update(_make_groups(filo)) else: option_groups.update({ arg: "" }) elif isinstance(arg, list): - logger.debug("_sanitise(): Got arg list: %s" % arg) + log.debug("_sanitise(): Got arg list: %s" % arg) arg.reverse() option_groups.update(_make_groups(arg)) else: - logger.debug("_sanitise(): Got non str or list arg: %s" % arg) + log.warn("_sanitise(): Got non-str/list arg: '%s', type '%s'" + % (arg, type(arg))) checked = _check_groups(option_groups) sanitised = ' '.join(x for x in checked) return sanitised else: - logger.debug("_sanitise(): Got None for args") + log.debug("_sanitise(): Got None for args") def _sanitise_list(arg_list): """A generator for iterating through a list of gpg options and sanitising diff --git a/src/util.py b/src/util.py index 262c755..092f882 100644 --- a/src/util.py +++ b/src/util.py @@ -24,7 +24,6 @@ Extra utilities for python-gnupg. from datetime import datetime -import logging import os import time import random @@ -32,6 +31,8 @@ import string import sys import threading +import _logger + try: from io import StringIO from io import BytesIO @@ -44,9 +45,10 @@ except: class NullHandler(logging.Handler): def handle(self, record): pass -logger = logging.getLogger('gnupg') -if not logger.handlers: - logger.addHandler(NullHandler()) +log = _logger.create_logger(0) +if not log.handlers: + log.addHandler(NullHandler()) + try: unicode @@ -79,7 +81,7 @@ def _copy_data(instream, outstream): # # or isinstance(instream, file)), "instream not stream or file" # assert isinstance(outstream, file), "outstream is not a file" #except AssertionError as ae: - # logger.exception(ae) + # log.exception(ae) # return if hasattr(sys.stdin, 'encoding'): @@ -92,7 +94,7 @@ def _copy_data(instream, outstream): if len(data) == 0: break sent += len(data) - logger.debug("_copy_data(): sending chunk (%d):\n%s" + log.debug("_copy_data(): sending chunk (%d):\n%s" % (sent, data[:256])) try: outstream.write(data) @@ -100,20 +102,20 @@ def _copy_data(instream, outstream): try: outstream.write(data.encode(enc)) except IOError: - logger.exception( + log.exception( '_copy_data(): Error sending data: Broken pipe') break except IOError: # Can get 'broken pipe' errors even when all data was sent - logger.exception('_copy_data(): Error sending data: Broken pipe') + log.exception('_copy_data(): Error sending data: Broken pipe') break try: outstream.close() except IOError: - logger.exception('_copy_data(): Got IOError while closing %s' + log.exception('_copy_data(): Got IOError while closing %s' % outstream) else: - logger.debug("_copy_data(): Closed output, %d bytes sent." % sent) + log.debug("_copy_data(): Closed output, %d bytes sent." % sent) def _create_homedir(homedir): """Create the specified GnuPG home directory, if necessary. @@ -123,20 +125,22 @@ def _create_homedir(homedir): :returns: True if no errors occurred and the directory was created or existed beforehand, False otherwise. """ - ## xxx how will this work in a virtualenv? if not os.path.isabs(homedir): message = ("Got non-abs gpg home dir path: %s" % homedir) - logger.warn("util._create_homedir(): %s" % message) + log.debug("util._create_homedir(): %s" % message) homedir = os.path.abspath(homedir) if not os.path.isdir(homedir): message = ("Creating gpg home dir: %s" % homedir) - logger.warn("util._create_homedir(): %s" % message) + log.debug("util._create_homedir():") + log.info("%s" % message) + try: os.makedirs(homedir, 0x1C0) except OSError as ose: - logger.error(ose, exc_info=1) + log.error(ose, exc_info=1) return False else: + log.debug("util._create_homedir(): Created directory.") return True else: return True @@ -157,7 +161,8 @@ def _find_binary(binary=None): if binary is not None: if not os.path.isabs(binary): try: binary = _which(binary)[0] - except IndexError as ie: logger.debug(ie.message) + except IndexError as ie: + log.debug("_find_binary(): %s" % ie.message) if binary is None: try: binary = _which('gpg')[0] except IndexError: raise RuntimeError("GnuPG is not installed!") @@ -166,7 +171,7 @@ def _find_binary(binary=None): assert not os.path.islink(binary), "Path to gpg binary is symlink" assert os.access(binary, os.X_OK), "Lacking +x perms for gpg binary" except (AssertionError, AttributeError) as ae: - logger.debug("util._find_binary(): %s" % ae.message) + log.debug("util._find_binary(): %s" % ae.message) else: return binary @@ -194,7 +199,7 @@ def _is_file(input): try: assert os.lstat(input).st_size > 0, "not a file: %s" % input except (AssertionError, TypeError, IOError, OSError) as error: - logger.debug(error.message) + log.debug(error.message) return False else: return True @@ -267,7 +272,7 @@ def _make_passphrase(length=None, save=False, file=None): os.chmod(file, 0600) os.chown(file, ruid, gid) - logger.warn("Generated passphrase saved to %s" % file) + log.warn("Generated passphrase saved to %s" % file) return passphrase def _make_random_string(length): @@ -302,7 +307,7 @@ def _threaded_copy_data(instream, outstream): copy_thread = threading.Thread(target=_copy_data, args=(instream, outstream)) copy_thread.setDaemon(True) - logger.debug('_threaded_copy_data(): %r, %r, %r', copy_thread, + log.debug('_threaded_copy_data(): %r, %r, %r', copy_thread, instream, outstream) copy_thread.start() return copy_thread @@ -349,4 +354,4 @@ def _write_passphrase(stream, passphrase, encoding): passphrase = '%s\n' % passphrase passphrase = passphrase.encode(encoding) stream.write(passphrase) - logger.debug("_write_passphrase(): Wrote passphrase.") + log.debug("_write_passphrase(): Wrote passphrase.") diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 7eb7c9f..2924611 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -9,9 +9,9 @@ Copyright Ā© 2008-2013 Vinay Sajip. All rights reserved. import argparse import doctest -import logging from functools import wraps import io +import logging import os import shutil import sys @@ -27,9 +27,9 @@ else: import gnupg from gnupg import parsers from gnupg import util +from gnupg import _logger - -logger = logging.getLogger('gnupg') +log = _logger.create_logger(10) _here = os.path.join(os.getcwd(), 'tests') _files = os.path.join(_here, 'files') _tempd = os.path.join(_here, 'tmp') @@ -37,6 +37,8 @@ _tempd = os.path.join(_here, 'tmp') tempfile.tempdir = _tempd if not os.path.isdir(tempfile.gettempdir()): os.mkdir(tempfile.gettempdir()) + log.debug("Creating temporary testing directory: %s" + % tempfile.gettempdir()) @wraps(tempfile.TemporaryFile) def _make_tempfile(*args, **kwargs): @@ -129,6 +131,7 @@ class GPGTestCase(unittest.TestCase): def setUp(self): """This method is called once per self.test_* method.""" + log.warn("\r%s" % str("=" * 78)) hd = tempfile.mkdtemp() if os.path.exists(hd): self.assertTrue(os.path.isdir(hd), "Not a directory: %s" % hd) @@ -144,10 +147,11 @@ class GPGTestCase(unittest.TestCase): try: shutil.rmtree(self.homedir) except OSError as ose: - logger.error(ose) + log.error(ose) else: - logger.warn("Can't delete homedir: '%s' not a directory" + log.warn("Can't delete homedir: '%s' not a directory" % self.homedir) + log.warn("%s" % str("=" * 78)) def test_parsers_fix_unsafe(self): """Test that unsafe inputs are quoted out and then ignored.""" @@ -454,8 +458,8 @@ class GPGTestCase(unittest.TestCase): ascii = ascii.replace("\r", "").strip() match = compare_keys(ascii, KEYS_TO_IMPORT) if match: - logger.debug("was: %r", KEYS_TO_IMPORT) - logger.debug("now: %r", ascii) + log.info("was: %r", KEYS_TO_IMPORT) + log.info("now: %r", ascii) self.assertEqual(0, match, "Keys must match") #Generate a key so we can test exporting private keys @@ -480,8 +484,8 @@ class GPGTestCase(unittest.TestCase): ascii = ascii.replace("\r", "").strip() match = compare_keys(ascii, KEYS_TO_IMPORT) if match: - logger.debug("was: %r", KEYS_TO_IMPORT) - logger.debug("now: %r", ascii) + log.info("was: %r", KEYS_TO_IMPORT) + log.info("now: %r", ascii) self.assertEqual(0, match, "Keys must match") def test_signature_string(self): @@ -538,8 +542,8 @@ class GPGTestCase(unittest.TestCase): verified = self.gpg.verify(sig.data) self.assertIsNotNone(verified.fingerprint) if key.fingerprint != verified.fingerprint: - logger.debug("key: %r", key.fingerprint) - logger.debug("ver: %r", verified.fingerprint) + log.warn("key fingerprint: %r", key.fingerprint) + log.warn("verified fingerprint: %r", verified.fingerprint) self.assertEqual(key.fingerprint, verified.fingerprint, "Fingerprints must match") self.assertEqual(verified.status, 'signature valid') @@ -611,7 +615,7 @@ class GPGTestCase(unittest.TestCase): public_keys = self.gpg.list_keys() self.assertTrue(is_list_with_len(public_keys, 1), "1-element list expected, got %d" % len(public_keys)) - logger.debug("test_deletion ends") + log.debug("test_deletion ends") def test_encryption(self): """Test encryption of a message string.""" @@ -685,8 +689,8 @@ authentication.""" encrypted = str(self.gpg.encrypt(message, ruck)) decrypted = self.gpg.decrypt(encrypted, passphrase="ruck") if message != decrypted.data: - logger.debug("was: %r", message) - logger.debug("new: %r", decrypted.data) + log.debug("was: %r", message) + log.debug("new: %r", decrypted.data) self.assertEqual(message, decrypted.data) def test_decryption_multi_recipient(self): @@ -723,9 +727,9 @@ be doing it in the first place. - Eric Schmidt, CEO of Google""" def test_file_encryption_and_decryption(self): """Test that encryption/decryption to/from file works.""" encfname = _make_tempfile() - logger.debug('Created tempfile for encrypted content: %s' % encfname) + log.debug('Created tempfile for encrypted content: %s' % encfname) decfname = _make_tempfile() - logger.debug('Created tempfile for decrypted content: f%s' % decfname) + log.debug('Created tempfile for decrypted content: f%s' % decfname) # On Windows, if the handles aren't closed, the files can't be deleted #os.close(encfno) #os.close(decfno) @@ -745,8 +749,8 @@ be doing it in the first place. - Eric Schmidt, CEO of Google""" ddata = decfname.read() data = data.encode(self.gpg.encoding) if ddata != data: - logger.debug("was: %r", data) - logger.debug("new: %r", ddata) + log.debug("was: %r", data) + log.debug("new: %r", ddata) self.assertEqual(data, ddata) @@ -800,16 +804,11 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'import': set(['test_import_only']), } def _init_logging(): + now = datetime.now().strftime("%Y-%m-%d_%H:%M:%S") logging.basicConfig( - level=logging.DEBUG, filename="test_gnupg.log", + filename=os.path.join(_here, "%s_test_gnupg.log" % now), filemode="a", format="%(asctime)s %(levelname)-5s %(name)-7s %(threadName)-10s %(message)s") - logging.captureWarnings(True) - logging.logThreads = True - stream_handler = logging.StreamHandler(stream=sys.stdout) - stream_handler.setLevel(logging.DEBUG) - logger.addHandler(stream_handler) - logger.debug("Starting the logger...") def main(args): if not args.quiet: @@ -822,20 +821,20 @@ def main(args): if args.test is not None: for suite in args.test: if suite in args.suites.keys(): - logger.debug("Adding %d items from test suite '%s':" + log.debug("Adding %d items from test suite '%s':" % (len(args.suites[suite]), suite)) for method in args.suites[suite]: load_tests.append(method) - logger.debug("\t%s" % method) + log.debug("\t%s" % method) else: - logger.debug("Ignoring unknown test suite %r" % suite) + log.debug("Ignoring unknown test suite %r" % suite) tests = unittest.TestSuite(list(map(GPGTestCase, load_tests))) else: tests = prog.testLoader.loadTestsFromTestCase(GPGTestCase) args.run_doctest = True if args.run_doctest: tests.addTest(doctest.DocTestSuite(gnupg)) - logger.debug("Loaded %d tests..." % tests.countTestCases()) + log.debug("Loaded %d tests..." % tests.countTestCases()) prog.test = tests runner = unittest.TextTestRunner(verbosity=args.verbose, stream=sys.stderr) From 81312015ab46046a48610fbd24b087fa903db206 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 02:38:25 +0000 Subject: [PATCH 239/397] Remove everything except the GPG class from src/__init__.py. --- src/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index b565e07..3084a11 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -6,7 +6,6 @@ from gnupg import GPG from ._version import get_versions __version__ = get_versions()['version'] -del get_versions gnupg.__version__ = __version__ gnupg.__author__ = 'Isis Agora Lovecruft' @@ -14,7 +13,7 @@ gnupg.__contact__ = 'isis@leap.se' gnupg.__url__ = 'https://github.com/isislovecruft/python-gnupg' gnupg.__license__ = copyleft.disclaimer -__all__ = ["gnupg", "copyright", - "Crypt", "DeleteResult", "ListKeys", - "GenKey", "Sign", "Encrypt", "ImportResult", "Verify"] +__all__ = ["GPG"] + del copyleft +del get_versions From fa2db06a3c90cb4154f8ab5615c3eaeca9537148 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 02:39:08 +0000 Subject: [PATCH 240/397] Add "del gnupg" in src/__init__.py to get rid of gnupg.gnupg in API. --- src/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/__init__.py b/src/__init__.py index 3084a11..5cd8a1a 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -15,5 +15,6 @@ gnupg.__license__ = copyleft.disclaimer __all__ = ["GPG"] +del gnupg del copyleft del get_versions From f5f67cef4cc5b8fc62c251b05c943a4a0ffdb07d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 02:40:20 +0000 Subject: [PATCH 241/397] Remove script headers from src/util.py. --- src/util.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/util.py b/src/util.py index 092f882..1c0f224 100644 --- a/src/util.py +++ b/src/util.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python -#-*- encoding: utf-8 -*- # # This file is part of python-gnupg, a Python wrapper around GnuPG. # Copyright Ā© 2013 Isis Lovecruft, Andrej B. From 9f18699285cdd741f924015e6b554d27b539febf Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 02:40:49 +0000 Subject: [PATCH 242/397] Fix module docstring typo in src/util.py. --- src/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.py b/src/util.py index 1c0f224..2ed7189 100644 --- a/src/util.py +++ b/src/util.py @@ -15,7 +15,7 @@ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. ''' -utils.py +util.py ---------- Extra utilities for python-gnupg. ''' From 2abcf8a733dd4dc1169e9eecf93c7e7efafc9b30 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 02:41:39 +0000 Subject: [PATCH 243/397] Fix $PWD discovery to reflect the prior change to use src/ instead of gnupg/. --- src/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.py b/src/util.py index 2ed7189..8161996 100644 --- a/src/util.py +++ b/src/util.py @@ -56,7 +56,7 @@ except NameError: ## Directory shortcuts: _here = os.getcwd() ## .../python-gnupg/src -_repo = _here.rsplit(__module__, 1)[0] ## .../python-gnupg +_repo = _here.rsplit('src', 1)[0] ## .../python-gnupg _test = os.path.join(os.path.join(_repo, 'tests'), 'tmp') ## .../python-gnupg/tests/tmp _user = os.environ.get('HOME') ## $HOME From c6880b5779d555b4a467f1fdcc078c84e62fbfb7 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 02:45:03 +0000 Subject: [PATCH 244/397] Rename util._create_homedir() to more general util._create_if_necessary(). --- src/util.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/util.py b/src/util.py index 8161996..2048db4 100644 --- a/src/util.py +++ b/src/util.py @@ -115,7 +115,7 @@ def _copy_data(instream, outstream): else: log.debug("_copy_data(): Closed output, %d bytes sent." % sent) -def _create_homedir(homedir): +def _create_if_necessary(directory): """Create the specified GnuPG home directory, if necessary. :param str homedir: The directory to use. @@ -123,25 +123,24 @@ def _create_homedir(homedir): :returns: True if no errors occurred and the directory was created or existed beforehand, False otherwise. """ - if not os.path.isabs(homedir): - message = ("Got non-abs gpg home dir path: %s" % homedir) - log.debug("util._create_homedir(): %s" % message) - homedir = os.path.abspath(homedir) - if not os.path.isdir(homedir): - message = ("Creating gpg home dir: %s" % homedir) - log.debug("util._create_homedir():") - log.info("%s" % message) + if not os.path.isabs(directory): + message = ("Got non-absolute path: %s" % directory) + log.debug("util._create_if_necessary(): %s" % message) + directory = os.path.abspath(directory) + + if not os.path.isdir(directory): + message = ("Creating directory: %s" % directory) + log.debug("util._create_if_necessary():") + log.info("%s" % message) try: - os.makedirs(homedir, 0x1C0) + os.makedirs(directory, 0x1C0) except OSError as ose: log.error(ose, exc_info=1) return False else: - log.debug("util._create_homedir(): Created directory.") - return True - else: - return True + log.debug("util._create_if_necessary(): Created directory.") + return True def _find_binary(binary=None): """Find the absolute path to the GnuPG binary. From 89e9e9baac1b6863c4c5f9e2d0c3ad1e1f2b4212 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 02:46:33 +0000 Subject: [PATCH 245/397] Update util._create_if_necessary() docstring. --- src/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util.py b/src/util.py index 2048db4..e37eb1b 100644 --- a/src/util.py +++ b/src/util.py @@ -116,9 +116,9 @@ def _copy_data(instream, outstream): log.debug("_copy_data(): Closed output, %d bytes sent." % sent) def _create_if_necessary(directory): - """Create the specified GnuPG home directory, if necessary. + """Create the specified directory, if necessary. - :param str homedir: The directory to use. + :param str directory: The directory to use. :rtype: bool :returns: True if no errors occurred and the directory was created or existed beforehand, False otherwise. From 88b9fad430f954f262f9ef3d3de3ccbacaa10cba Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 12:38:36 +0000 Subject: [PATCH 246/397] Fix encoding string. --- src/_logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_logger.py b/src/_logger.py index eddf9f2..16fff60 100644 --- a/src/_logger.py +++ b/src/_logger.py @@ -1,4 +1,4 @@ -#-*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- # # This file is part of python-gnupg, a Python wrapper around GnuPG. # Copyright Ā© 2013 Isis Lovecruft, Andrej B. From c1bd8bdbab2fc90e62ee8ba48ba8cad03e5bc6fb Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 12:38:59 +0000 Subject: [PATCH 247/397] Remove unnecessary module.__name__. --- src/_logger.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/_logger.py b/src/_logger.py index 16fff60..525c35b 100644 --- a/src/_logger.py +++ b/src/_logger.py @@ -19,7 +19,6 @@ ---------- Logging module for python-gnupg. ''' -__name__ = '_logger' from functools import wraps From 7a9e8a55bd1af158724603cdfa29cd6cadb55fd8 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 12:40:09 +0000 Subject: [PATCH 248/397] Remove unused ctypes import. --- src/_logger.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/_logger.py b/src/_logger.py index 525c35b..0e31747 100644 --- a/src/_logger.py +++ b/src/_logger.py @@ -20,9 +20,10 @@ Logging module for python-gnupg. ''' -from functools import wraps +from __future__ import print_function +from datetime import datetime +from functools import wraps -import ctypes import logging import os import sys From f4e2ed69dc95d735b4a0141446161d493a8c5a05 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 12:40:42 +0000 Subject: [PATCH 249/397] Import _ansiterm without using module namespace. --- src/_logger.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/_logger.py b/src/_logger.py index 0e31747..a2d850a 100644 --- a/src/_logger.py +++ b/src/_logger.py @@ -25,8 +25,10 @@ from datetime import datetime from functools import wraps import logging -import os import sys +import os + +import _ansistrm try: from logging import NullHandler From 9bd451d7da4ab650a072d7e5a37f4484cc13d345 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Fri, 17 May 2013 12:41:36 +0000 Subject: [PATCH 250/397] Fix encoding strings and remove script directive. --- src/_ansistrm.py | 1 + src/gnupg.py | 3 +-- src/util.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/_ansistrm.py b/src/_ansistrm.py index b4c8020..6453d23 100644 --- a/src/_ansistrm.py +++ b/src/_ansistrm.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # # This file is part of python-gnupg, a Python wrapper aroung GnuPG, and it was # taken from https://gist.github.com/vsajip/758430 on the 14th of May, 2013. It diff --git a/src/gnupg.py b/src/gnupg.py index 1f59b14..c3b323a 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python -#-*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- # # This file is part of python-gnupg, a Python interface to GnuPG. # Copyright Ā© 2013 Isis Lovecruft diff --git a/src/util.py b/src/util.py index e37eb1b..616ef92 100644 --- a/src/util.py +++ b/src/util.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # # This file is part of python-gnupg, a Python wrapper around GnuPG. # Copyright Ā© 2013 Isis Lovecruft, Andrej B. From 36511c4727b300894651454921c927a687f17b52 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 04:29:16 +0000 Subject: [PATCH 251/397] Rename parsers.py to _parsers.py. --- src/{parsers.py => _parsers.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{parsers.py => _parsers.py} (100%) diff --git a/src/parsers.py b/src/_parsers.py similarity index 100% rename from src/parsers.py rename to src/_parsers.py From 904154029db649e72875aaa81e487920b0593bb7 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 04:30:36 +0000 Subject: [PATCH 252/397] Rename util.py to _util.py. --- src/{util.py => _util.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{util.py => _util.py} (100%) diff --git a/src/util.py b/src/_util.py similarity index 100% rename from src/util.py rename to src/_util.py From a13ca557968555f69e069b3e95bfe89f53da5202 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 04:33:47 +0000 Subject: [PATCH 253/397] Remove old log setup code, use result of _logger.create_logger(). --- src/_util.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/_util.py b/src/_util.py index 616ef92..8748fe7 100644 --- a/src/_util.py +++ b/src/_util.py @@ -38,23 +38,13 @@ try: except ImportError: from cStringIO import StringIO -try: - from logging import NullHandler -except: - class NullHandler(logging.Handler): - def handle(self, record): - pass -log = _logger.create_logger(0) -if not log.handlers: - log.addHandler(NullHandler()) - - try: unicode _py3k = False except NameError: _py3k = True + ## Directory shortcuts: _here = os.getcwd() ## .../python-gnupg/src _repo = _here.rsplit('src', 1)[0] ## .../python-gnupg @@ -65,6 +55,9 @@ _ugpg = os.path.join(_user, '.gnupg') ## $HOME/.gnupg _conf = os.path.join(os.path.join(_user, '.config'), 'python-gnupg') ## $HOME/.config/python-gnupg +## Logger is disabled by default +log = _logger.create_logger(0) + def _copy_data(instream, outstream): """Copy data from one stream to another. From d3b190f4f23e8a631628ee123d1930616e9a5922 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 04:34:26 +0000 Subject: [PATCH 254/397] Test that clients' Python handles the basestring builtin correctly. --- src/_util.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/_util.py b/src/_util.py index 8748fe7..acb9b70 100644 --- a/src/_util.py +++ b/src/_util.py @@ -41,6 +41,12 @@ except ImportError: try: unicode _py3k = False + try: + isinstance(__name__, basestring) + except NameError: + msg = "Sorry, python-gnupg requires a Python version with proper" + msg += " unicode support. Please upgrade to Python>=2.3." + raise SystemExit(msg) except NameError: _py3k = True From 4ffe36823009ab17b110831fd80d74fed911e270 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 04:35:45 +0000 Subject: [PATCH 255/397] Update directory shortcuts in utils to reflect path changes. --- src/_util.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/_util.py b/src/_util.py index acb9b70..178d959 100644 --- a/src/_util.py +++ b/src/_util.py @@ -52,14 +52,12 @@ except NameError: ## Directory shortcuts: -_here = os.getcwd() ## .../python-gnupg/src -_repo = _here.rsplit('src', 1)[0] ## .../python-gnupg -_test = os.path.join(os.path.join(_repo, 'tests'), - 'tmp') ## .../python-gnupg/tests/tmp -_user = os.environ.get('HOME') ## $HOME -_ugpg = os.path.join(_user, '.gnupg') ## $HOME/.gnupg -_conf = os.path.join(os.path.join(_user, '.config'), - 'python-gnupg') ## $HOME/.config/python-gnupg +_here = os.getcwd() +_test = os.path.join(os.path.join(_here, 'tests'), 'tmp') ## ./tests/tmp +_user = os.environ.get('HOME') ## $HOME +_ugpg = os.path.join(_user, '.gnupg') ## $HOME/.gnupg +_conf = os.path.join(os.path.join(_user, '.config'), 'python-gnupg') + ## $HOME/.config/python-gnupg ## Logger is disabled by default log = _logger.create_logger(0) From f1a6526b5ce7d1d2538d9ef1a6fedc336ae41f3f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 04:37:02 +0000 Subject: [PATCH 256/397] Update log statements in _util.py. --- src/_util.py | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/_util.py b/src/_util.py index 178d959..5c5a7f2 100644 --- a/src/_util.py +++ b/src/_util.py @@ -82,6 +82,7 @@ def _copy_data(instream, outstream): if hasattr(sys.stdin, 'encoding'): enc = sys.stdin.encoding + log.debug("Obtained encoding from stdin: %s" % enc) else: enc = 'ascii' @@ -90,28 +91,26 @@ def _copy_data(instream, outstream): if len(data) == 0: break sent += len(data) - log.debug("_copy_data(): sending chunk (%d):\n%s" - % (sent, data[:256])) + log.debug("Sending chunk %d bytes:\n%s" + % (sent, data)) try: outstream.write(data) except UnicodeError: try: outstream.write(data.encode(enc)) except IOError: - log.exception( - '_copy_data(): Error sending data: Broken pipe') + log.exception("Error sending data: Broken pipe") break except IOError: # Can get 'broken pipe' errors even when all data was sent - log.exception('_copy_data(): Error sending data: Broken pipe') + log.exception('Error sending data: Broken pipe') break try: outstream.close() - except IOError: - log.exception('_copy_data(): Got IOError while closing %s' - % outstream) + except IOError as ioe: + log.error("Unable to close outstream %s:\r\t%s" % (outstream, ioe)) else: - log.debug("_copy_data(): Closed output, %d bytes sent." % sent) + log.debug("Closed outstream: %d bytes sent." % sent) def _create_if_necessary(directory): """Create the specified directory, if necessary. @@ -123,13 +122,11 @@ def _create_if_necessary(directory): """ if not os.path.isabs(directory): - message = ("Got non-absolute path: %s" % directory) - log.debug("util._create_if_necessary(): %s" % message) + log.debug("Got non-absolute path: %s" % directory) directory = os.path.abspath(directory) if not os.path.isdir(directory): - message = ("Creating directory: %s" % directory) - log.debug("util._create_if_necessary():") + log.debug("Creating directory: %s" % directory) log.info("%s" % message) try: os.makedirs(directory, 0x1C0) @@ -137,7 +134,7 @@ def _create_if_necessary(directory): log.error(ose, exc_info=1) return False else: - log.debug("util._create_if_necessary(): Created directory.") + log.debug("Created directory.") return True def _find_binary(binary=None): @@ -157,7 +154,7 @@ def _find_binary(binary=None): if not os.path.isabs(binary): try: binary = _which(binary)[0] except IndexError as ie: - log.debug("_find_binary(): %s" % ie.message) + log.error(ie.message) if binary is None: try: binary = _which('gpg')[0] except IndexError: raise RuntimeError("GnuPG is not installed!") @@ -166,7 +163,7 @@ def _find_binary(binary=None): assert not os.path.islink(binary), "Path to gpg binary is symlink" assert os.access(binary, os.X_OK), "Lacking +x perms for gpg binary" except (AssertionError, AttributeError) as ae: - log.debug("util._find_binary(): %s" % ae.message) + log.error(ae.message) else: return binary @@ -193,8 +190,8 @@ def _is_file(input): """ try: assert os.lstat(input).st_size > 0, "not a file: %s" % input - except (AssertionError, TypeError, IOError, OSError) as error: - log.debug(error.message) + except (AssertionError, TypeError, IOError, OSError) as err: + log.error(err.message, exc_info=1) return False else: return True @@ -302,8 +299,7 @@ def _threaded_copy_data(instream, outstream): copy_thread = threading.Thread(target=_copy_data, args=(instream, outstream)) copy_thread.setDaemon(True) - log.debug('_threaded_copy_data(): %r, %r, %r', copy_thread, - instream, outstream) + log.debug('%r, %r, %r', copy_thread, instream, outstream) copy_thread.start() return copy_thread @@ -349,4 +345,4 @@ def _write_passphrase(stream, passphrase, encoding): passphrase = '%s\n' % passphrase passphrase = passphrase.encode(encoding) stream.write(passphrase) - log.debug("_write_passphrase(): Wrote passphrase.") + log.debug("Wrote passphrase on stdin.") From 6874f7b83829e0972db65a6b0055ba84672ae984 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 04:37:30 +0000 Subject: [PATCH 257/397] Add docstring for _util._write_passphrase() function. --- src/_util.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/_util.py b/src/_util.py index 5c5a7f2..a02251c 100644 --- a/src/_util.py +++ b/src/_util.py @@ -342,6 +342,14 @@ def _which(executable, flags=os.X_OK): return result def _write_passphrase(stream, passphrase, encoding): + """Write the passphrase from memory to the GnuPG process' stdin. + + :type stream: file, :class:BytesIO, or :class:StringIO + :param stream: The input file descriptor to write the password to. + :param str passphrase: The passphrase for the secret key material. + :param str encoding: The data encoding expected by GnuPG. Usually, this + is ``sys.getfilesystemencoding()``. + """ passphrase = '%s\n' % passphrase passphrase = passphrase.encode(encoding) stream.write(passphrase) From c0b599ee069b3fd3beb9b62e4efb74063e3594d5 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 04:38:30 +0000 Subject: [PATCH 258/397] Remove interpreter directive from _parsers.py. --- src/_parsers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index 83006c6..e2b440d 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python -#-*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- # # This file is part of python-gnupg, a Python wrapper around GnuPG. # Copyright Ā© 2013 Isis Lovecruft From 1dab5c681ed248193fc2b50e1c2aaa8247f25f5f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 04:38:57 +0000 Subject: [PATCH 259/397] Credit drebs in _parsers.py too. --- src/_parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_parsers.py b/src/_parsers.py index e2b440d..16afffe 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of python-gnupg, a Python wrapper around GnuPG. -# Copyright Ā© 2013 Isis Lovecruft +# Copyright Ā© 2013 Isis Lovecruft, Andrej B. # Ā© 2008-2012 Vinay Sajip # Ā© 2005 Steve Traugott # Ā© 2004 A.M. Kuchling From 4857a475bcdfe9c5247934cb66fd5ca10485b77d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 04:39:31 +0000 Subject: [PATCH 260/397] Update import statements in _parsers.py with util->_util. --- src/_parsers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index 16afffe..fc2bf70 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -23,8 +23,9 @@ Classes for parsing GnuPG status messages and sanitising commandline options. import re -import util -from util import log +from _util import log + +import _util ESCAPE_PATTERN = re.compile(r'\\x([0-9a-f][0-9a-f])', re.I) From de8dd5ac1e77ea8b53b683c66d2e1b7d09630a68 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 04:48:03 +0000 Subject: [PATCH 261/397] Update imports in gnupg.py with parsers->_parsers and util->_util. --- src/gnupg.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index c3b323a..3207c8b 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -94,7 +94,14 @@ import sys import tempfile import threading -from util import log, _conf +from _parsers import _fix_unsafe, _sanitise, _is_allowed, _sanitise_list +from _parsers import _check_preferences +from _util import _conf, _is_list_or_tuple, _is_stream +from _util import _make_binary_stream +from _util import log + +import _util +import _parsers import parsers From 4ea188423939bca8eea660ac4f9c4125c723a62d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 14:46:41 +0000 Subject: [PATCH 262/397] Update .gitigore to exclude symlinked gpg binaries. --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f9987a1..75b16e9 100644 --- a/.gitignore +++ b/.gitignore @@ -61,4 +61,7 @@ tickets/* # Ignore virtualenv folders, if they are here: include/* -local/* \ No newline at end of file +local/* + +# Ignore gpg binary symlinks: +gpg From a4630e1cfbfa35a4c555a7f66b9d29218b56fd2f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 14:49:09 +0000 Subject: [PATCH 263/397] Add InheritablePropery class for mutable, inheritable descriptors. * This allows us to define immutable x.getter and x.setter methods in a parent class while still inheriting the ability to alter the descriptor in a child class without needing to set something like: @property def x(self): super(self.__class__, self)._get_x() as a property override for every descriptor. --- src/_util.py | 36 ++++++++++++++++++++++++++++++++++++ src/gnupg.py | 1 + 2 files changed, 37 insertions(+) diff --git a/src/_util.py b/src/_util.py index a02251c..d2a0ef9 100644 --- a/src/_util.py +++ b/src/_util.py @@ -354,3 +354,39 @@ def _write_passphrase(stream, passphrase, encoding): passphrase = passphrase.encode(encoding) stream.write(passphrase) log.debug("Wrote passphrase on stdin.") + + +class InheritableProperty(object): + """Based on the emulation of PyProperty_Type() in Objects/descrobject.c""" + + def __init__(self, fget=None, fset=None, fdel=None, doc=None): + self.fget = fget + self.fset = fset + self.fdel = fdel + self.__doc__ = doc + + def __get__(self, obj, objtype=None): + if obj is None: + return self + if self.fget is None: + raise AttributeError, "unreadable attribute" + if self.fget.__name__ == '' or not self.fget.__name__: + return self.fget(obj) + else: + return getattr(obj, self.fget.__name__)() + + def __set__(self, obj, value): + if self.fset is None: + raise AttributeError, "can't set attribute" + if self.fset.__name__ == '' or not self.fset.__name__: + self.fset(obj, value) + else: + getattr(obj, self.fset.__name__)(value) + + def __delete__(self, obj): + if self.fdel is None: + raise AttributeError, "can't delete attribute" + if self.fdel.__name__ == '' or not self.fdel.__name__: + self.fdel(obj) + else: + getattr(obj, self.fdel.__name__)() diff --git a/src/gnupg.py b/src/gnupg.py index 3207c8b..b09fcef 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -96,6 +96,7 @@ import threading from _parsers import _fix_unsafe, _sanitise, _is_allowed, _sanitise_list from _parsers import _check_preferences +from _util import InheritableProperty from _util import _conf, _is_list_or_tuple, _is_stream from _util import _make_binary_stream from _util import log From 848e410f1974d0eebce824775c90df7ec309524d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 14:57:53 +0000 Subject: [PATCH 264/397] Add MetaGPG and GPGBase for handling attribute/property setup. * These classes also play a few other tricks, like the steps taken to disable pinentry if GPG.use_agent=False: 1) detect if gpg-agent is running for the same EUID as python, and if it is then 2) find out if the user wants us to use it, and if they don't then 3) find if pinentry is installed, and if it is then 4) modify the system PATH to exclude the directory where pinentry is, symlinking out gpg if gpg was in the same directory, and then 5) register an _exithandler with the interpreter to replace the original environment when the interpreter exits. --- requirements.txt | 1 + src/gnupg.py | 254 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 252 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 50feaef..c82f9d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ Sphinx>=1.1 +psutil>=0.5.1 diff --git a/src/gnupg.py b/src/gnupg.py index b09fcef..98da167 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -78,9 +78,12 @@ try: except ImportError: from cStringIO import StringIO +from pprint import pprint +from psutil import process_iter from subprocess import Popen from subprocess import PIPE +import atexit import codecs ## For AOS, the locale module will need to point to a wrapper around the ## java.util.Locale class. @@ -104,13 +107,258 @@ from _util import log import _util import _parsers -import parsers + +class GPGMeta(type): + """Metaclass for changing the :meth:GPG.__init__ initialiser. + + Detects running gpg-agent processes and the presence of a pinentry + program, and disables pinentry so that python-gnupg can write the + passphrase to the controlled GnuPG process without killing the agent. + """ + + def __new__(cls, name, bases, attrs): + """Construct the initialiser for GPG""" + log.debug("Metaclass __new__ constructor called for %r" % cls) + if cls._find_agent(): + ## call the normal GPG.__init__() initialisor: + attrs['init'] = cls.__init__ ## nothing changed for now + attrs['_remove_agent'] = True + return super(GPGMeta, cls).__new__(cls, name, bases, attrs) + + @classmethod + def _find_agent(cls): + """Discover if a gpg-agent process for the current euid is running. + + If there is a matching gpg-agent process, set a :class:psutil.Process + instance containing the gpg-agent process' information to + :attr:cls._agent_proc. + + :returns: True if there exists a gpg-agent process running under the + same effective user ID as that of this program. Otherwise, + returns None. + """ + identity = os.getresuid() + for proc in process_iter(): + if (proc.name == "gpg-agent") and proc.is_running: + log.debug("Found gpg-agent process with pid %d" % proc.pid) + if proc.uids == identity: + log.debug( + "Effective UIDs of this process and gpg-agent match") + setattr(cls, '_agent_proc', proc) + return True + + ## xxx we might not need this, try setting: + ## attrs['remove_path'] = __remove_path__ + + # @classmethod + # def _init_decorator(cls): + # """Wraps the :meth:__init__ function in a partial of itself.""" + # log.debug("_init_decorator called for %s" % cls.__init__.__repr__()) + # def _init_wrapper(*args, **kwargs): + # wraps(cls.__init__, *args, **kwargs) + # if getattr(cls, '_agent_proc', None) is not None: + # cls.__remove_path__(prog='pinentry') + # return _init_wrapper -import util as _util +class GPGBase(object): + """Base class to control process initialisation and for property storage.""" + + __metaclass__ = GPGMeta + + def __init__(self, binary=None, home=None, keyring=None, secring=None, + use_agent=False, default_preference_list=None, + verbose=False, options=None): + + self.binary = _util._find_binary(binary) + self.homedir = home if home else _conf + pub = _fix_unsafe(keyring) if keyring else 'pubring.gpg' + sec = _fix_unsafe(secring) if secring else 'secring.gpg' + self.keyring = os.path.join(self._homedir, pub) + self.secring = os.path.join(self._homedir, sec) + self._prefs = 'SHA512 SHA384 SHA256 AES256 CAMELLIA256 TWOFISH ZLIB ZIP' + self.options = _sanitise(options) if options else None + + self.encoding = locale.getpreferredencoding() + if self.encoding is None: # This happens on Jython! + self.encoding = sys.stdin.encoding + + try: + assert self.binary, "Could not find binary %s" % binary + assert isinstance(verbose, (bool, str, int)), \ + "'verbose' must be boolean, string, or 0 <= n <= 9" + assert isinstance(use_agent, bool), "'use_agent' must be boolean" + + if self.options is not None: + assert isinstance(self.options, str), "options not string" + except (AssertionError, AttributeError) as ae: + log.error("GPGBase.__init__(): %s" % ae.message) + raise RuntimeError(ae.message) + else: + self.verbose = verbose + self.use_agent = use_agent + + if hasattr(self, '_agent_proc') \ + and getattr(self, '_remove_agent', None) is True: + if hasattr(self, '__remove_path__'): + self.__remove_path__('pinentry') + + def __remove_path__(self, prog=None): + log.debug("Attempting to remove %s from system PATH" % str(prog)) + if (prog is None) or (not isinstance(prog, str)): return + + try: + program = _util._which(prog)[0] + except (OSError, IOerror, IndexError) as err: + log.err(err.message) + log.err("Cannot find program '%s', not changing PATH." % prog) + return + + ## __remove_path__ cannot be an @classmethod in GPGMeta, because + ## the use_agent attribute must be set by the instance. + if not self.use_agent: + program_path = os.path.dirname(program) + + ## symlink our gpg binary into the current working directory + if os.path.dirname(self.gpg.binary) == program_path: + os.symlink(self.gpg.binary, os.path.join(os.getcwd(), 'gpg')) + + ## copy the original environment so we can put it back later: + env_copy = os.environ + path_copy = os.environ.pop('PATH') + assert not os.environ.has_key('PATH') + log.debug("Created a copy of system PATH: %r" % path_copy) + + path_string = '{}:'.format(program_path) + rm_program = path_copy.replace(path_string, None) + + @staticmethod + def update_path(env_copy, path_value): + log.debug("Updating system path...") + os.environ = env_copy + os.environ.update({'PATH': path_value}) + log.debug("System $PATH: %s" % os.environ['PATH']) + + update_path(env_copy, rm_program) + + ## register an _exithandler with the python interpreter: + atexit.register(update_path, env_copy, path_copy) + + # @property + # def keyring(self): + # """Get the public keyring.""" + # return self._keyring + # + # @keyring.setter + # def keyring(self, pub): + # """Set the file to use as GnuPG's current (public) keyring. + # + # :param str pub: The filename, relative to :attr:``GPG.homedir``, to use + # for storing public key data. + # """ + # ring = _fix_unsafe(pub) if pub else 'pubring.gpg' + # self._keyring = os.path.join(self._homedir, ring) + # + # @property + # def secring(self): + # """Get the secret keyring.""" + # return self._secring + # + # @secring.setter + # def secring(self, sec): + # """Set the file to use as GnuPG's current secret keyring. + # + # :param str pub: The filename, relative to :attr:``GPG.homedir``, to use + # for storing secret key data. + # """ + # ring = _fix_unsafe(sec) if sec else 'secring.gpg' + # self._secring = os.path.join(self._homedir, ring) + + @property + def default_preference_list(self): + """Get the default preference list.""" + return self._prefs + + @default_preference_list.setter + def default_preference_list(self, prefs): + """Set the default preference list. + + :param str prefs: A string containing the default preferences for + ciphers, digests, and compression algorithms. + """ + prefs = _check_preferences(prefs) + if prefs is not None: + self._prefs = prefs + + @default_preference_list.deleter + def default_preference_list(self, prefs): + """Reset the default preference list to its original state. + + Note that "original state" does not mean the default preference + list for whichever version of GnuPG is being used. It means the + default preference list defined by :attr:`GPGBase._preferences`. + + Using BZIP2 is avoided due to not interacting well with some versions + of GnuPG>=2.0.0. + """ + self._prefs = 'SHA512 SHA384 SHA256 AES256 CAMELLIA256 TWOFISH ZLIB ZIP' -class GPG(object): + def _homedir_getter(self): + """Get the directory currently being used as GnuPG's homedir. + + If unspecified, use $HOME/.config/python-gnupg/ + + :rtype: str + :returns: The absolute path to the current GnuPG homedir. + """ + return self._homedir + + def _homedir_setter(self, directory): + """Set the directory to use as GnuPG's homedir. + + If unspecified, use $HOME/.config/python-gnupg. If specified, ensure + that the ``directory`` does not contain various shell escape + characters. If ``directory`` is not found, it will be automatically + created. Lastly, the ``direcory`` will be checked that the EUID has + read and write permissions for it. + + :param str homedir: A relative or absolute path to the directory to use + for storing/accessing GnuPG's files, including + keyrings and the trustdb. + :raises: :exc:`RuntimeError` if unable to find a suitable directory to + use. + """ + if not directory: + log.debug("GPGBase._homedir_setter(): Using default homedir: '%s'" + % _conf) + directory = _conf + + hd = _fix_unsafe(directory) + log.debug("GPGBase._homedir_setter(): got directory '%s'" % hd) + + if hd: + log.debug("GPGBase._homedir_setter(): Check existence of '%s'" % hd) + _util._create_if_necessary(hd) + + try: + log.debug("GPGBase._homedir_setter(): checking permissions") + assert _util._has_readwrite(hd), \ + "Homedir '%s' needs read/write permissions" % hd + except AssertionError as ae: + msg = ("Unable to set '%s' as GnuPG homedir" % directory) + log.debug("GPGBase.homedir.setter(): %s" % msg) + log.debug(ae.message) + raise RuntimeError(ae.message) + else: + log.debug("GPGBase:") + log.info("Setting homedir to '%s'" % hd) + self._homedir = hd + + homedir = InheritableProperty(_homedir_getter, _homedir_setter) + + +class GPG(GPGBase): """Encapsulate access to the gpg executable""" _decode_errors = 'strict' From a3d2616a6cc1efce7ed03a73462d44533a8c6c24 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 15:08:54 +0000 Subject: [PATCH 265/397] Add a a log formatter and a new log level for GnuPG internal status codes. --- src/_logger.py | 57 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/src/_logger.py b/src/_logger.py index a2d850a..6fe6a66 100644 --- a/src/_logger.py +++ b/src/_logger.py @@ -37,31 +37,60 @@ except: def handle(self, record): pass -import gnupg._ansistrm -#log = logging.getLogger('gnupg') -#if not log.handlers: -# log.addHandler(NullHandler()) +GNUPG_STATUS_LEVEL = 9 +def status(self, message, *args, **kwargs): + """LogRecord for GnuPG internal status messages.""" + if self.isEnabledFor(GNUPG_STATUS_LEVEL): + self._log(GNUPG_STATUS_LEVEL, message, args, **kwargs) @wraps(logging.Logger) 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 + :param level: A string or an integer for the lowest level to log. + Available levels: + int str description + 0 NOTSET Disable all logging. + 9 GNUPG Log GnuPG's internal status messages. + 10 DEBUG Log module level debuging messages. + 20 INFO Normal user-level messages. + 30 WARN Warning messages. + 40 ERROR Error messages and tracebacks. + 50 CRITICAL Unhandled exceptions and tracebacks. + """ + _test = os.path.join(os.getcwd(), 'tests') + _now = datetime.now().strftime("%Y-%m-%d_%H%M%S") + _fn = os.path.join(_test, "%s_test_gnupg.log" % _now) + _fmt = "%(relativeCreated)-4d L%(lineno)-4d:%(funcName)-18.18s %(levelname)-7.7s %(message)s" + + logging.basicConfig(level=level, filename=_fn, filemode="a", format=_fmt) + ## Add the GNUPG_STATUS_LEVEL LogRecord to all Loggers in the module: + logging.addLevelName(GNUPG_STATUS_LEVEL, "GNUPG") + logging.Logger.status = status - log = logging.getLogger('gnupg') if level > logging.NOTSET: logging.captureWarnings(True) logging.logThreads = True - log.setLevel(level) - colorizer = gnupg._ansistrm.ColorizingStreamHandler(stream=sys.stdout) - colorizer.setLevel(level) - log.addHandler(colorizer) + colouriser = _ansistrm.ColorizingStreamHandler + colouriser.level_map[9] = (None, 'blue', False) + colouriser.level_map[10] = (None, 'cyan', False) + handler = colouriser(stream=sys.stderr) + handler.setLevel(level) - log.debug("Starting the logger...") - - if not log.handlers: - log.addHandler(NullHandler()) + formatr = logging.Formatter(_fmt) + handler.setFormatter(formatr) + print("Starting the logger...") + else: + handler = NullHandler() + print("GnuPG logging disabled...") + log = logging.getLogger('gnupg') + log.addHandler(handler) + log.setLevel(level) + log.info("Log opened: %s UTC" % datetime.ctime(datetime.utcnow())) return log From 5cd2e19361137e5a4a2c9dfa9bb1b54a37a7ffd9 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 15:12:00 +0000 Subject: [PATCH 266/397] Minor and unnecessary obsessive compulsive code formatting change. --- src/_parsers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_parsers.py b/src/_parsers.py index fc2bf70..e3c96ac 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -376,7 +376,8 @@ def _is_allowed(input): '--export', '--export-secret-keys', '--export-secret-subkeys', - '--fingerprint',]) + '--fingerprint', + ]) ## check that allowed is a subset of possible try: From 931713eca3d80b6081445a0662bb8bfa73b763f0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 15:27:20 +0000 Subject: [PATCH 267/397] Add TODO file with ideas for future improvements. --- TODO | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ src/_parsers.py | 6 ++++- src/gnupg.py | 10 ++++++--- 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 TODO diff --git a/TODO b/TODO new file mode 100644 index 0000000..d752f6d --- /dev/null +++ b/TODO @@ -0,0 +1,60 @@ +-*- mode: org -*- + +* Keyring separation :keyseparation: +** TODO in GPG.gen_key() :keyseparation:gen_key: +It would be nice to have an option for gen_key() [[gnupg.py:927]] to +automatically switch before key generation to a new tempfile.mkdtemp() +directory, with a new keyring and secring, and then to rename either the +directory or the keyrings with the long keyid of the key which was freshly +generated. + +* I/O :io: +** TODO in GPG.__make_args() :io:makeargs: +It would be nice to make the file descriptors for communication with the GnuPG +process configurable, and not the default, hard-coded 0=stdin 1=stdout +2=stderr. + +* Key editing :editkey: +** TODO add '--edit-key' feature :editkey: +see :compatibility:gen__key_input: + +* Compatibility between GnuPG versions :compatibility: +** TODO GnuPG>=2.1.0 won't allow key generation with preset passphrase +*** TODO in GPG.gen__key_input() :compatibility:gen_key_input: +In the docstring of GPG.gen__key_input() [[gnupg.py:1068]], for the parameter +'passphrase', it is explained that: + + :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 you to specify a + passphrase for batch key generation -- GnuPG + will ignore the ``passphrase`` parameter, stop, + and ask the user for the new passphrase. + However, we can put the command '%no-protection' + into the batch key generation file to allow a + passwordless key to be created, which can then + have its passphrase set later with '--edit-key'. + +If we add a GnuPG version detection feature (the version string is already +obtained in GPG.___init___() [[gnupg.py:407]]), then we can automatically chain +GPG.gen__key_input() to another new feature for '--edit-key'. This chaining +would likely need to happen here [[gnupg.py:1146]]. + +*** TODO add '--edit-key' feature :editkey: +This would be necessary for adding a passphrase to the key after passwordless +generation in GnuPG>=2.1.0. + +* Code cleanup :cleanup: +** TODO in parsers.__sanitise() :cleanup:sanitise: +Ughh...this is the ugliest code I think I've ever written. It works, but I +worry that it is fragile, not to mention *I* have trouble reading it, and I +fucking wrote the damn thing. There's probably not much that could be done to +make it more Pythonic, because type checks and input validation are pretty much +intrinsically non-Pythonic. But did i mention that it's ugly? I'm sure these +functions would be pretty glad to get a shower, shave, and haircut. + +** TODO in parsers.__is_allowed() :cleanup:is_allowed: +There is a lot of madness dealing with stupid things like hyphens +vs. underscores, and lists of options vs. strings. This can *definitely* be +cleaned up. + diff --git a/src/_parsers.py b/src/_parsers.py index e3c96ac..a39f70c 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -389,6 +389,8 @@ def _is_allowed(input): raise UsageError(ae.message) ## if we got a list of args, join them + ## + ## see TODO file, tag :cleanup: if not isinstance(input, str): input = ' '.join([x for x in input]) @@ -448,6 +450,8 @@ def _sanitise(*args): :returns: ``sanitised`` """ + ## see TODO file, tag :cleanup:sanitise: + def _check_option(arg, value): """ Check that a single :param:arg is an allowed option. If it is allowed, @@ -1151,7 +1155,7 @@ class ListPackets(object): :raises: :exc:`ValueError` if the status message is unknown. """ - # TODO: write tests for _handle_status + # TODO: write tests for handle_status if key == 'NODATA': self.nodata = True elif key == 'ENC_TO': diff --git a/src/gnupg.py b/src/gnupg.py index 98da167..cd5b38a 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -457,7 +457,9 @@ class GPG(GPGBase): :func:parsers._sanitise. The ``passphrase`` argument needs to be True if a passphrase will be sent to GPG, else False. """ - cmd = [self.binary, '--status-fd 2 --no-tty --no-emit-version'] + ## see TODO file, tag :io:makeargs: + cmd = [self.binary, '--no-emit-version --no-tty --status-fd 2'] + if self.homedir: cmd.append('--homedir "%s"' % self.homedir) if self.keyring: @@ -916,6 +918,7 @@ class GPG(GPGBase): :returns: The result mapping with details of the new key, which is a :class:`parsers.GenKey ` object. """ + ## see TODO file, tag :gen_key: for todo items args = ["--gen-key --batch"] key = self._result_map['generate'](self) f = _util._make_binary_stream(input, self.encoding) @@ -1051,8 +1054,6 @@ class GPG(GPGBase): passwordless key to be created, which can then have its passphrase set later with '--edit-key'. - ## TODO add version detection and add the '%no-protection' flag. - :param str preferences: Set the cipher, hash, and compression preference values for this key. This expects the same type of string as the sub-command @@ -1119,6 +1120,9 @@ class GPG(GPGBase): out += "%%secring %s\n" % self.secring if testing: + ## see TODO file, tag :compatibility:gen_key_input: + ## + ## Add version detection before the '%no-protection' flag. out += "%no-protection\n" out += "%transient-key\n" From 8b154c3df5d82214b48bee858d57d86c0592dff9 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 15:29:19 +0000 Subject: [PATCH 268/397] Update result map to point to _parsers. --- src/gnupg.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index cd5b38a..99f4192 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -360,16 +360,16 @@ class GPGBase(object): class GPG(GPGBase): """Encapsulate access to the gpg executable""" - _decode_errors = 'strict' - _result_map = { 'crypt': parsers.Crypt, - 'delete': parsers.DeleteResult, - 'generate': parsers.GenKey, - 'import': parsers.ImportResult, - 'list': parsers.ListKeys, - 'sign': parsers.Sign, - 'verify': parsers.Verify, - 'packets': parsers.ListPackets } + _decode_errors = 'strict' + _result_map = { 'crypt': _parsers.Crypt, + 'delete': _parsers.DeleteResult, + 'generate': _parsers.GenKey, + 'import': _parsers.ImportResult, + 'list': _parsers.ListKeys, + 'sign': _parsers.Sign, + 'verify': _parsers.Verify, + 'packets': _parsers.ListPackets } def __init__(self, binary=None, homedir=None, verbose=False, use_agent=False, keyring=None, secring=None, From a887be1a1702b8b0d323275b767a82941e930fb2 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 15:30:16 +0000 Subject: [PATCH 269/397] Update docstring for GPG. --- src/gnupg.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 99f4192..2c232df 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -377,24 +377,31 @@ class GPG(GPGBase): """Initialize a GnuPG process wrapper. :param str binary: Name for GnuPG binary executable. If the absolute - path is not given, the evironment variable $PATH - is searched for the executable and checked that - the real uid/gid of the user has sufficient - permissions. + path is not given, the evironment variable $PATH is + searched for the executable and checked that the + real uid/gid of the user has sufficient permissions. + :param str homedir: Full pathname to directory containing the public and private keyrings. Default is whatever GnuPG defaults to. - :param str keyring: raises :exc:DeprecationWarning. Use :param:pubring. + + :param str keyring: Name of keyring file containing public key data, if + unspecified, defaults to 'pubring.gpg' in the + ``homedir`` directory. + :param str secring: Name of alternative secret keyring file to use. If left unspecified, this will default to using 'secring.gpg' in the :param:homedir directory, and create that file if it does not exist. + :param str pubring: Name of alternative public keyring file to use. If left unspecified, this will default to using 'pubring.gpg' in the :param:homedir directory, and create that file if it does not exist. + :param list options: A list of additional options to pass to the GPG binary. + :raises: :exc:`RuntimeError` with explanation message if there is a problem invoking gpg. """ From 7d345d9e2b6e47d95ce82b8ef77d41474ecc4e50 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 16:44:40 +0000 Subject: [PATCH 270/397] Update a couple log statements and comments in _parsers.py. --- src/_parsers.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index a39f70c..82ad872 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -441,8 +441,8 @@ def _sanitise(*args): For information on the PGP message format specification, see: https://www.ietf.org/rfc/rfc1991.txt - If you're asking, "Is this *really* necessary?": No. Not really. See: - https://xkcd.com/1181/ + If you're asking, "Is this *really* necessary?": No, not really -- we could + just do a check as described here: https://xkcd.com/1181/ :param str args: (optional) The boolean arguments which will be passed to the GnuPG process. @@ -489,8 +489,7 @@ def _sanitise(*args): if util._is_file(val): safe_option += (val + " ") else: - log.debug("_check_option(): %s not file: %s" - % (flag, val)) + log.debug("%s not file: %s" % (flag, val)) elif flag in ['--default-key', '--recipient', '--export', '--export-secret-keys', '--delete-keys', @@ -498,8 +497,7 @@ def _sanitise(*args): if _is_hex(val): safe_option += (val + " ") else: - log.debug("_check_option(): '%s %s' not hex." - % (flag, val)) + log.debug("'%s %s' not hex." % (flag, val)) else: safe_option += (val + " ") log.debug("_check_option(): No checks for %s" @@ -519,7 +517,7 @@ def _sanitise(*args): while len(filo) >= 1: last = filo.pop() if is_flag(last): - log.debug("_make_groups(): Got arg: %s" % last) + log.debug("Got arg: %s" % last) if last == '--verify': groups[last] = str(filo.pop()) ## accept the read-from-stdin arg: @@ -528,12 +526,12 @@ def _sanitise(*args): else: groups[last] = str() while len(filo) > 1 and not is_flag(filo[len(filo)-1]): - log.debug("_make_groups(): Got value: %s" + log.debug("Got value: %s" % filo[len(filo)-1]) groups[last] += (filo.pop() + " ") else: if len(filo) == 1 and not is_flag(filo[0]): - log.debug("_make_groups(): Got value: %s" % filo[0]) + log.debug("Got value: %s" % filo[0]) groups[last] += filo.pop() else: log.warn("_make_groups(): Got solitary value: %s" % last) @@ -541,16 +539,16 @@ def _sanitise(*args): return groups def _check_groups(groups): - log.debug("_check_groups(): Got groups: %s" % groups) + log.debug("Got groups: %s" % groups) checked_groups = [] for a,v in groups.items(): v = None if len(v) == 0 else v safe = _check_option(a, v) if safe is not None and not safe.strip() == "": - log.debug("_check_groups(): appending option: %s" % safe) + log.debug("Appending option: %s" % safe) checked_groups.append(safe) else: - log.warn("_check_groups(): dropped option '%s %s'" % (a,v)) + log.warn("Dropped option: '%s %s'" % (a,v)) return checked_groups if args is not None: @@ -558,25 +556,26 @@ def _sanitise(*args): for arg in args: ## if we're given a string with a bunch of options in it split ## them up and deal with them separately - if isinstance(arg, str): - log.debug("_sanitise(): Got arg string: %s" % arg) + if (not _util._py3k and isinstance(arg, basestring)) \ + or (_util._py3k and isinstance(arg, str)): + log.debug("Got arg string: %s" % arg) if arg.find(' ') > 0: filo = _make_filo(arg) option_groups.update(_make_groups(filo)) else: option_groups.update({ arg: "" }) elif isinstance(arg, list): - log.debug("_sanitise(): Got arg list: %s" % arg) + log.debug("Got arg list: %s" % arg) arg.reverse() option_groups.update(_make_groups(arg)) else: - log.warn("_sanitise(): Got non-str/list arg: '%s', type '%s'" + log.warn("Got non-str/list arg: '%s', type '%s'" % (arg, type(arg))) checked = _check_groups(option_groups) sanitised = ' '.join(x for x in checked) return sanitised else: - log.debug("_sanitise(): Got None for args") + log.debug("Got None for args") def _sanitise_list(arg_list): """A generator for iterating through a list of gpg options and sanitising From 691d0163d41e0df7328a57cd91e87a0fba4d9475 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Sat, 18 May 2013 17:14:26 +0000 Subject: [PATCH 271/397] Rewrite GPG.__init__() to take advantage of the Meta and Base classes. --- src/gnupg.py | 78 +++++++++++++++++++--------------------------------- 1 file changed, 29 insertions(+), 49 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 2c232df..d4843b1 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -406,57 +406,37 @@ class GPG(GPGBase): problem invoking gpg. """ - if not homedir: - homedir = _conf - self.homedir = _fix_unsafe(homedir) - if self.homedir: - _util._create_homedir(self.homedir) - else: - message = ("Unsuitable gpg home dir: %s" % homedir) - log.debug("GPG.__init__(): %s" % message) + super(GPG, self).__init__( + binary=binary, + home=homedir, + keyring=keyring, + secring=secring, + default_preference_list=default_preference_list, + options=options, + verbose=verbose, + use_agent=use_agent,) - self.binary = _util._find_binary(binary) + log.info(""" +Initialised settings: +binary: %s +homedir: %s +keyring: %s +secring: %s +default_preference_list: %s +options: %s +verbose: %s +use_agent: %s + """ % (self.binary, self.homedir, self.keyring, self.secring, + self.default_preference_list, self.options, str(self.verbose), + str(self.use_agent))) - if default_preference_list is None: - prefs = 'SHA512 SHA384 SHA256 AES256 CAMELLIA256 TWOFISH ZLIB ZIP' - else: - prefs = _check_preferences(default_preference_list) - self.default_preference_list = prefs - - secring = 'secring.gpg' if secring is None else _fix_unsafe(secring) - keyring = 'pubring.gpg' if keyring is None else _fix_unsafe(keyring) - self.secring = os.path.join(self.homedir, secring) - self.keyring = os.path.join(self.homedir, keyring) - - self.options = _sanitise(options) if options else None - - self.encoding = locale.getpreferredencoding() - if self.encoding is None: # This happens on Jython! - self.encoding = sys.stdin.encoding - - try: - assert self.homedir is not None, "Got None for self.homedir" - assert _util._has_readwrite(self.homedir), ("Home dir %s needs r+w" - % self.homedir) - assert self.binary, "Could not find binary %s" % full - assert isinstance(verbose, bool), "'verbose' must be boolean" - assert isinstance(use_agent, bool), "'use_agent' must be boolean" - if self.options: - assert isinstance(options, str), ("options not formatted: %s" - % options) - except (AssertionError, AttributeError) as ae: - log.debug("GPG.__init__(): %s" % ae.message) - raise RuntimeError(ae.message) - else: - self.verbose = verbose - self.use_agent = use_agent - - proc = self._open_subprocess(["--version"]) - result = self._result_map['list'](self) - self._collect_output(proc, result, stdin=proc.stdin) - if proc.returncode != 0: - raise RuntimeError("Error invoking gpg: %s: %s" - % (proc.returncode, result.stderr)) + ## check that everything runs alright: + proc = self._open_subprocess(["--version"]) + result = self._result_map['list'](self) + self._collect_output(proc, result, stdin=proc.stdin) + if proc.returncode != 0: + raise RuntimeError("Error invoking gpg: %s: %s" + % (proc.returncode, result.stderr)) def _make_args(self, args, passphrase=False): """Make a list of command line elements for GPG. The value of ``args`` From 205d5394c71b25fd4011531bef3bbab6d614bd48 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 20 May 2013 03:05:41 +0000 Subject: [PATCH 272/397] Add tagfile creation commands for etags and ctags to Makefile. * TODO We should probably not use a Makefile...but it's so handy. --- .gitignore | 1 + Makefile | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 75b16e9..244111b 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ nosetests.xml # tags TAGS +tags # notes *.org diff --git a/Makefile b/Makefile index 49172ae..a6a7b13 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,10 @@ +ctags: + ctags -R *.py + +etags: + find . -name "*.py" -print | xargs etags + cleanup-src: cd src && \ rm -f \#*\# && \ From bedaa0ca4911a0c36bd982fa8ee71e03bc77cd36 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 12:16:33 +0000 Subject: [PATCH 273/397] Switch to a slightly more useful output as the startup check. --- src/_parsers.py | 1 + src/gnupg.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_parsers.py b/src/_parsers.py index 82ad872..c4862d0 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -365,6 +365,7 @@ def _is_allowed(input): '--compress-algo', '--compression-algo', '--cert-digest-algo', + '--list-config', '--personal-digest-prefs', '--personal-digest-preferences', '--personal-cipher-prefs', diff --git a/src/gnupg.py b/src/gnupg.py index d4843b1..e5b513e 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -431,7 +431,7 @@ use_agent: %s str(self.use_agent))) ## check that everything runs alright: - proc = self._open_subprocess(["--version"]) + proc = self._open_subprocess(["--list-config", "--with-colons"]) result = self._result_map['list'](self) self._collect_output(proc, result, stdin=proc.stdin) if proc.returncode != 0: From d5753278fc95f6191c1140c04bfc59988c40f1cf Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 12:17:47 +0000 Subject: [PATCH 274/397] Actually parse and validate user-set cipher/hash/digest preferences. --- src/gnupg.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index e5b513e..4c5fb29 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -176,12 +176,14 @@ class GPGBase(object): sec = _fix_unsafe(secring) if secring else 'secring.gpg' self.keyring = os.path.join(self._homedir, pub) self.secring = os.path.join(self._homedir, sec) - self._prefs = 'SHA512 SHA384 SHA256 AES256 CAMELLIA256 TWOFISH ZLIB ZIP' self.options = _sanitise(options) if options else None - self.encoding = locale.getpreferredencoding() - if self.encoding is None: # This happens on Jython! - self.encoding = sys.stdin.encoding + if default_preference_list: + self._prefs = _check_options(default_preference_list, 'all') + else: + self._prefs = 'SHA512 SHA384 SHA256 AES256 CAMELLIA256 TWOFISH' + self._prefs += ' AES192 ZLIB ZIP Uncompressed' + try: assert self.binary, "Could not find binary %s" % binary From fe531bcd91679b88b77367392bc8b2ddea51bb20 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 12:20:58 +0000 Subject: [PATCH 275/397] Add original encoding discovery code into GPGBase, but ensure it's lowercased. --- src/gnupg.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index 4c5fb29..3a8d402 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -184,13 +184,17 @@ class GPGBase(object): self._prefs = 'SHA512 SHA384 SHA256 AES256 CAMELLIA256 TWOFISH' self._prefs += ' AES192 ZLIB ZIP Uncompressed' + encoding = locale.getpreferredencoding() + if encoding is None: # This happens on Jython! + encoding = sys.stdin.encoding + self.encoding = encoding.lower().replace('-', '_') + try: assert self.binary, "Could not find binary %s" % binary assert isinstance(verbose, (bool, str, int)), \ "'verbose' must be boolean, string, or 0 <= n <= 9" assert isinstance(use_agent, bool), "'use_agent' must be boolean" - if self.options is not None: assert isinstance(self.options, str), "options not string" except (AssertionError, AttributeError) as ae: From 14c48b2f093453f230e9679fddee377a8e3d7415 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 12:22:16 +0000 Subject: [PATCH 276/397] Add docstring for GPGBase._remove_path() method. --- src/gnupg.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index 3a8d402..009fa2f 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -209,7 +209,15 @@ class GPGBase(object): if hasattr(self, '__remove_path__'): self.__remove_path__('pinentry') - def __remove_path__(self, prog=None): + def __remove_path__(self, prog=None, at_exit=True): + """Remove a the directories containing a program from the system's + $PATH. If self.gpg.binary is in a directory being removed, it is + symlinked to './gpg' + + :param str prog: + """ + self._removed_path_entries = [] + log.debug("Attempting to remove %s from system PATH" % str(prog)) if (prog is None) or (not isinstance(prog, str)): return From ddce4fbc3b9de7654140cac5c7e95f38b0129d85 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 12:23:01 +0000 Subject: [PATCH 277/397] Fix a bug due to a typo in the name of an Exception class. --- src/gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index 009fa2f..3713ff6 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -223,7 +223,7 @@ class GPGBase(object): try: program = _util._which(prog)[0] - except (OSError, IOerror, IndexError) as err: + except (OSError, IOError, IndexError) as err: log.err(err.message) log.err("Cannot find program '%s', not changing PATH." % prog) return From a34fd1c4237e3cdb1b1cd4a7dc79814016373896 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 12:25:03 +0000 Subject: [PATCH 278/397] Discover if gpg.binary resides in a directory we've purged from the path. --- src/gnupg.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 3713ff6..408b476 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -231,20 +231,20 @@ class GPGBase(object): ## __remove_path__ cannot be an @classmethod in GPGMeta, because ## the use_agent attribute must be set by the instance. if not self.use_agent: - program_path = os.path.dirname(program) + program_base = os.path.dirname(prog) + gnupg_base = os.path.dirname(self.binary) - ## symlink our gpg binary into the current working directory - if os.path.dirname(self.gpg.binary) == program_path: - os.symlink(self.gpg.binary, os.path.join(os.getcwd(), 'gpg')) + ## symlink our gpg binary into $PWD if the path we are removing is + ## the one which contains our gpg executable: + if gnupg_base == program_base: + os.symlink(self.binary, os.path.join(os.getcwd(), 'gpg')) - ## copy the original environment so we can put it back later: - env_copy = os.environ + ## copy the original environment so that we can put it back later: + env_copy = os.environ ## this one should not be touched path_copy = os.environ.pop('PATH') - assert not os.environ.has_key('PATH') log.debug("Created a copy of system PATH: %r" % path_copy) + assert not os.environ.has_key('PATH'), "OS env kept $PATH anyway!" - path_string = '{}:'.format(program_path) - rm_program = path_copy.replace(path_string, None) @staticmethod def update_path(env_copy, path_value): From f67270609f4264237626d3d59b85c02cd5636056 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 12:26:02 +0000 Subject: [PATCH 279/397] Add remove_from_path() function for removing an entry from the system PATH. * All removed entries should have an @atexit hook to be reinserted into the system PATH before the Python interpreter exits. --- src/gnupg.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/gnupg.py b/src/gnupg.py index 408b476..9788efc 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -245,6 +245,26 @@ class GPGBase(object): log.debug("Created a copy of system PATH: %r" % path_copy) assert not os.environ.has_key('PATH'), "OS env kept $PATH anyway!" + @staticmethod + def remove_program_from_path(path, prog_base): + """Remove all directories which contain a program from PATH. + + :param str path: The contents of the system environment's + PATH. + :param str prog_base: The base (directory only) portion of a + program's location. + """ + paths = path.split(':') + for directory in paths: + if directory == prog_base: + log.debug("Found directory with target program: %s" + % directory) + path.remove(directory) + self._removed_path_entries.append(directory) + log.debug("Deleted all found instance of %s." % directory) + log.debug("PATH is now:%s%s" % (os.linesep, path)) + new_path = ':'.join([p for p in path]) + return new_path @staticmethod def update_path(env_copy, path_value): From a8023e7eabaa3488d6043e3b8d1824a8e7905cfc Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 12:28:14 +0000 Subject: [PATCH 280/397] Add docstring for update_path() method. --- src/gnupg.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index 9788efc..14a57d7 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -267,7 +267,12 @@ class GPGBase(object): return new_path @staticmethod - def update_path(env_copy, path_value): + def update_path(environment, path): + """Add paths to the string at os.environ['PATH']. + + :param str environment: The environment mapping to update. + :param list path: A list of strings to update the PATH with. + """ log.debug("Updating system path...") os.environ = env_copy os.environ.update({'PATH': path_value}) From b2b0d07c5eea0da0964641cbb950521b61223b56 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 12:29:31 +0000 Subject: [PATCH 281/397] Add logic for finding which PATHs to remove, store, and then re-add. --- src/gnupg.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 14a57d7..d5aa32b 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -274,15 +274,27 @@ class GPGBase(object): :param list path: A list of strings to update the PATH with. """ log.debug("Updating system path...") - os.environ = env_copy - os.environ.update({'PATH': path_value}) + os.environ = environment + new_path = ':'.join([p for p in path]) + old = '' + if 'PATH' in os.environ: + new_path = ':'.join([os.environ['PATH'], new_path]) + os.environ.update({'PATH': new_path}) log.debug("System $PATH: %s" % os.environ['PATH']) - update_path(env_copy, rm_program) + modified_path = remove_program_from_path(path_copy, program_base) + update_path(env_copy, modified_path) ## register an _exithandler with the python interpreter: atexit.register(update_path, env_copy, path_copy) + @atexit.register + def remove_symlinked_binary(): + loc = os.path.join(os.getcwd(), 'gpg') + if os.path.islink(loc): + os.unline(loc) + log.debug("Removed binary symlink '%s'" % loc) + # @property # def keyring(self): # """Get the public keyring.""" From 5891730de4b37ac4a0f5ef8da43ad81125ceadbe Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 12:35:30 +0000 Subject: [PATCH 282/397] Add batch query limiting attribute. * This prevents commands with excessive output from generating an unexpected IOError. --- src/gnupg.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/gnupg.py b/src/gnupg.py index d5aa32b..c5c0dcc 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -421,6 +421,9 @@ class GPG(GPGBase): 'sign': _parsers.Sign, 'verify': _parsers.Verify, 'packets': _parsers.ListPackets } + #: The number of simultaneous keyids we should list operations like + # '--list-sigs' to: + _batch_limit = 25 def __init__(self, binary=None, homedir=None, verbose=False, use_agent=False, keyring=None, secring=None, From 3b7734c32f74e5a9b934196221a96a5de50888d0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 12:39:01 +0000 Subject: [PATCH 283/397] Add doctest for GPG class. --- src/gnupg.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/gnupg.py b/src/gnupg.py index c5c0dcc..0ed263c 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -458,6 +458,26 @@ class GPG(GPGBase): :raises: :exc:`RuntimeError` with explanation message if there is a problem invoking gpg. + + Example: + + >>> import gnupg + GnuPG logging disabled... + >>> gpg = gnupg.GPG(homedir='./test-homedir') + >>> gpg.keyring + './test-homedir/pubring.gpg' + >>> gpg.secring + './test-homedir/secring.gpg' + >>> gpg.use_agent + False + >>> gpg.binary + '/usr/bin/gpg' + >>> import os + >>> import shutil + >>> if os.path.exists('./test-homedir'): + ... shutil.rmtree('./test-homedir') + ... + """ super(GPG, self).__init__( From b735333d7e8800c8b806e9806c8deeda3a7d4a08 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 12:40:45 +0000 Subject: [PATCH 284/397] Update docstring for GPG._make_args(). --- src/gnupg.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index 0ed263c..0d370ac 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -515,24 +515,42 @@ use_agent: %s def _make_args(self, args, passphrase=False): """Make a list of command line elements for GPG. The value of ``args`` will be appended only if it passes the checks in - :func:parsers._sanitise. The ``passphrase`` argument needs to be True + :func:`parsers._sanitise`. The ``passphrase`` argument needs to be True if a passphrase will be sent to GPG, else False. + + :param list args: A list of strings of options and flags to pass to + ``GPG.binary``. This is input safe, meaning that + these values go through strict checks (see + ``parsers._sanitise_list``) before being passed to to + the input file descriptor for the GnuPG process. + Each string should be given exactly as it would be on + the commandline interface to GnuPG, + e.g. ["--cipher-algo AES256", "--default-key + A3ADB67A2CDB8B35"]. + + :param bool passphrase: If True, the passphrase will be sent to the + stdin file descriptor for the attached GnuPG + process. """ ## see TODO file, tag :io:makeargs: cmd = [self.binary, '--no-emit-version --no-tty --status-fd 2'] if self.homedir: cmd.append('--homedir "%s"' % self.homedir) + if self.keyring: cmd.append('--no-default-keyring --keyring %s' % self.keyring) if self.secring: cmd.append('--secret-keyring %s' % self.secring) + if passphrase: cmd.append('--batch --passphrase-fd 0') + if self.use_agent: cmd.append('--use-agent') else: cmd.append('--no-use-agent') + if self.options: [cmd.append(opt) for opt in iter(_sanitise_list(self.options))] if args: From ebf51ecb5daff474d8b34f67e51c95ba7add0837 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 12:41:57 +0000 Subject: [PATCH 285/397] Add additional integer variables to GPG.verbose to enable getting GnuPG debugging. --- src/gnupg.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/gnupg.py b/src/gnupg.py index 0d370ac..98ce46a 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -555,6 +555,19 @@ use_agent: %s [cmd.append(opt) for opt in iter(_sanitise_list(self.options))] if args: [cmd.append(arg) for arg in iter(_sanitise_list(args))] + + if self.verbose: + if isinstance(self.verbose, str): + if self.verbose in ['basic', 'advanced', 'expert', 'guru']: + cmd.append('--debug-all') + cmd.append('--debug-level %s' % self.verbose) + elif isinstance(self.verbose, int) and (0 <= self.verbose <= 9): + if self.verbose >= 1: + cmd.append('--debug-all') + cmd.append('--debug-level %s' % self.verbose) + elif self.verbose is True: + cmd.append('--debug-all') + return cmd def _open_subprocess(self, args=None, passphrase=False): From 3b769c5202c927f340cd5419fde8021147847891 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 12:43:00 +0000 Subject: [PATCH 286/397] Update docst ring for GPG._open_subprocess(). --- src/gnupg.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/gnupg.py b/src/gnupg.py index 98ce46a..f514328 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -573,7 +573,23 @@ use_agent: %s def _open_subprocess(self, args=None, passphrase=False): """Open a pipe to a GPG subprocess and return the file objects for communicating with it. + + :param list args: A list of strings of options and flags to pass to + ``GPG.binary``. This is input safe, meaning that + these values go through strict checks (see + ``parsers._sanitise_list``) before being passed to to + the input file descriptor for the GnuPG process. + Each string should be given exactly as it would be on + the commandline interface to GnuPG, + e.g. ["--cipher-algo AES256", "--default-key + A3ADB67A2CDB8B35"]. + + :param bool passphrase: If True, the passphrase will be sent to the + stdin file descriptor for the attached GnuPG + process. """ + ## see http://docs.python.org/2/library/subprocess.html#converting-an\ + ## -argument-sequence-to-a-string-on-windows cmd = ' '.join(self._make_args(args, passphrase)) log.debug("_open_subprocess(): %s", cmd) return Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE) From 93cfc8a20e8fd869c8ee0623fd0ed68e61466127 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 12:44:37 +0000 Subject: [PATCH 287/397] Update several log statements. --- src/gnupg.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index f514328..ba2270f 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -591,7 +591,7 @@ use_agent: %s ## see http://docs.python.org/2/library/subprocess.html#converting-an\ ## -argument-sequence-to-a-string-on-windows cmd = ' '.join(self._make_args(args, passphrase)) - log.debug("_open_subprocess(): %s", cmd) + log.debug("Sending command to GnuPG process:%s%s" % (os.linesep, cmd)) return Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE) def _read_response(self, stream, result): @@ -608,20 +608,24 @@ use_agent: %s break lines.append(line) line = line.rstrip() - if self.verbose: - log.info("%s" % line) - else: - log.debug("%s" % line) if line[0:9] == '[GNUPG:] ': # Chop off the prefix line = line[9:] + log.status("%s" % line) L = line.split(None, 1) keyword = L[0] if len(L) > 1: value = L[1] else: value = "" - result._handle_status(keyword, value) + result.handle_status(keyword, value) + elif line[0:5] == 'gpg: ': + log.warn("%s" % line) + else: + if self.verbose: + log.info("%s" % line) + else: + log.debug("%s" % line) result.stderr = ''.join(lines) def _read_data(self, stream, result): @@ -631,7 +635,7 @@ use_agent: %s data = stream.read(1024) if len(data) == 0: break - log.debug("GPG._read_data(): read chunk: %r" % data[:256]) + log.debug("read from stdout: %r" % data[:256]) chunks.append(data) if _util._py3k: # Join using b'' or '', as appropriate @@ -648,13 +652,13 @@ use_agent: %s stderr = codecs.getreader(self.encoding)(process.stderr) rr = threading.Thread(target=self._read_response, args=(stderr, result)) rr.setDaemon(True) - log.debug('GPG._collect_output(): stderr reader: %r', rr) + log.debug('stderr reader: %r', rr) rr.start() stdout = process.stdout dr = threading.Thread(target=self._read_data, args=(stdout, result)) dr.setDaemon(True) - log.debug('GPG._collect_output(): stdout reader: %r', dr) + log.debug('stdout reader: %r', dr) dr.start() dr.join() @@ -989,7 +993,7 @@ use_agent: %s for line in lines: if self.verbose: print(line) - log.debug("line: %r", line.rstrip()) + log.debug("%r", line.rstrip()) if not line: break L = line.strip().split(':') From b5a0770d565dba23647acd0254d05cd3556a28e7 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 12:46:21 +0000 Subject: [PATCH 288/397] Update docstring for GPG.sign(). --- src/gnupg.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index ba2270f..0e34d1a 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -687,9 +687,37 @@ use_agent: %s self._collect_output(p, result, writer, stdin) return result - def sign(self, message, **kwargs): - """Create a signature for a message or file.""" - if isinstance(message, file): + def sign(self, data, **kwargs): + """Create a signature for a message string or file. + + Note that this method is not for signing other keys. (In GnuPG's terms, + what we all usually call 'keysigning' is actually termed + 'certification'...) Even though they are cryptographically the same + operation, GnuPG differentiates between them, presumedly because these + operations are also the same as the decryption operation. If the + ``key_usage``s ``C (certification)``, ``S (sign)``, and ``E + (encrypt)``, were all the same key, the key would "wear down" through + frequent signing usage -- since signing data is usually done often -- + meaning that the secret portion of the keypair, also used for + decryption in this scenario, would have a statistically higher + probability of an adversary obtaining an oracle for it (or for a + portion of the rounds in the cipher algorithm, depending on the family + of cryptanalytic attack used). + + In simpler terms: this function isn't for signing your friends' keys, + it's for something like signing an email. + + :type data: str or file + :param data: A string or file stream to sign. + :param str keyid: The key to sign with. + :param str passphrase: The passphrase to pipe to stdin. + :param bool clearsign: If True, create a cleartext signature. + :param bool detach: If True, create a detached signature. + :param bool binary: If True, do not ascii armour the output. + """ + if isinstance(data, file): + log.warn("Note: This function is not for signing other keys,") + log.warn(" see the docstring for GPG.sign()") if 'keyid' in kwargs.items(): log.info("Signing file '%r' with keyid: %s" % (data, kwargs[keyid])) From d9b7a85fe62b456d209c286955a95c5863799117 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 12:57:24 +0000 Subject: [PATCH 289/397] Change variable names in GPG.sign() to be clearer. --- src/gnupg.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 0e34d1a..729901d 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -723,16 +723,18 @@ use_agent: %s % (data, kwargs[keyid])) else: log.warn("No 'sign_with' keyid given! Using default key.") - result = self._sign_file(message, **kwargs) - elif not _util._is_stream(message): + result = self._sign_file(data, **kwargs) + + elif not _is_stream(data): if 'keyid' in kwargs.items(): log.info("Signing data string '%s' with keyid: %s" % (data, kwargs[keyid])) else: log.warn("No 'sign_with' keyid given! Using default key.") - f = _util._make_binary_stream(message, self.encoding) - result = self._sign_file(f, **kwargs) - f.close() + stream = _make_binary_stream(data, self.encoding) + result = self._sign_file(stream, **kwargs) + stream.close() + else: log.warn("Unable to sign message '%s' with type %s" % (data, type(data))) From 8af74f0981afe29fdc915987714a28e6045cb392 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 12:59:57 +0000 Subject: [PATCH 290/397] Update docstring for GPG._sign_file(). --- src/gnupg.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index 729901d..b3b431c 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -743,7 +743,15 @@ use_agent: %s def _sign_file(self, file, keyid=None, passphrase=None, clearsign=True, detach=False, binary=False): - """Create a signature for a file.""" + """Create a signature for a file. + + :param file: The file stream (i.e. it's already been open()'d) to sign. + :param str keyid: The key to sign with. + :param str passphrase: The passphrase to pipe to stdin. + :param bool clearsign: If True, create a cleartext signature. + :param bool detach: If True, create a detached signature. + :param bool binary: If True, do not ascii armour the output. + """ log.debug("_sign_file():") if binary: log.info("Creating binary signature for file %s" % file) From ece570fdda8d3370db7fe98e5c35f2c2bc73c685 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 13:02:29 +0000 Subject: [PATCH 291/397] =?UTF-8?q?Rename=20all=20calls=20=5Futils.=5Fmake?= =?UTF-8?q?=5Fbinary=5Fstream=E2=86=92=5Fmake=5Fbinary=5Fstream?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gnupg.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index b3b431c..1c58403 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -801,7 +801,7 @@ use_agent: %s >>> assert verify """ - f = _util._make_binary_stream(data, self.encoding) + f = _make_binary_stream(data, self.encoding) result = self.verify_file(f) f.close() return result @@ -894,10 +894,11 @@ use_agent: %s ## it might be possible to use --list-packets and parse the output result = self._result_map['import'](self) - log.debug('import_keys: %r', key_data[:256]) - data = _util._make_binary_stream(key_data, self.encoding) + log.info('Importing: %r', key_data[:256]) + data = _make_binary_stream(key_data, self.encoding) self._handle_io(['--import'], data, result, binary=True) - log.debug('import_keys result: %r', result.__dict__) + pretty = pprint(result.__dict__, indent=4, width=76, depth=8) + log.debug("Import result:%s%s" % (os.linesep, pretty)) data.close() return result @@ -914,7 +915,7 @@ use_agent: %s safe_keyserver = _fix_unsafe(keyserver) result = self._result_map['import'](self) - data = _util._make_binary_stream("", self.encoding) + data = _make_binary_stream("", self.encoding) args = ['--keyserver', keyserver, '--recv-keys'] if keyids: @@ -955,7 +956,7 @@ use_agent: %s if subkeys: which='secret-and-public-key' - if _util._is_list_or_tuple(fingerprints): + if _is_list_or_tuple(fingerprints): fingerprints = ' '.join(fingerprints) args = ['--batch'] @@ -980,7 +981,7 @@ use_agent: %s elif secret: which='-secret-keys' - if _util._is_list_or_tuple(keyids): + if _is_list_or_tuple(keyids): keyids = ' '.join(['%s' % k for k in keyids]) args = ["--armor"] @@ -1071,7 +1072,7 @@ use_agent: %s ## see TODO file, tag :gen_key: for todo items args = ["--gen-key --batch"] key = self._result_map['generate'](self) - f = _util._make_binary_stream(input, self.encoding) + f = _make_binary_stream(input, self.encoding) self._handle_io(args, f, key, binary=True) f.close() return key @@ -1397,9 +1398,9 @@ use_agent: %s >>> assert result.fingerprint == print1 """ - data = _util._make_binary_stream(data, self.encoding) - result = self.encrypt_file(data, recipients, **kwargs) - data.close() + stream = _make_binary_stream(data, self.encoding) + result = self.encrypt_file(stream, recipients, **kwargs) + stream.close() return result def decrypt(self, message, **kwargs): @@ -1407,9 +1408,9 @@ use_agent: %s :param message: A string or file-like object to decrypt. """ - data = _util._make_binary_stream(message, self.encoding) - result = self.decrypt_file(data, **kwargs) - data.close() + stream = _make_binary_stream(data, self.encoding) + result = self.decrypt_file(stream, **kwargs) + stream.close() return result def decrypt_file(self, file, always_trust=False, passphrase=None, From 60b97938f40b8cd03662a8c8e22524e45923577d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 13:03:31 +0000 Subject: [PATCH 292/397] Update a log statement. --- src/gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index 1c58403..4934fc1 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -993,7 +993,7 @@ use_agent: %s #stdout, stderr = p.communicate() result = self._result_map['delete'](self) # any result will do self._collect_output(p, result, stdin=p.stdin) - log.debug('export_keys result: %r', result.data) + log.debug('Exported:%s%r' % (os.linesep, result.data)) return result.data.decode(self.encoding, self._decode_errors) def list_keys(self, secret=False): From 1b95e254d72cc9fd57976bb7ae2c33094ddc1e04 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 13:03:58 +0000 Subject: [PATCH 293/397] Update docstring for GPG.list_keys(). --- src/gnupg.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/gnupg.py b/src/gnupg.py index 4934fc1..645f40f 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -999,6 +999,11 @@ use_agent: %s def list_keys(self, secret=False): """List the keys currently in the keyring. + The GnuPG option '--show-photos', according to the GnuPG manual, "does + not work with --with-colons", but since we can't rely on all versions + of GnuPG to explicitly handle this correctly, we should probably + include it in the args. + >>> import shutil >>> shutil.rmtree("keys") >>> gpg = GPG(homedir="keys") From 6400efa0e6946f9e69963ecf3031f3ca9617e03e Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 13:45:44 +0000 Subject: [PATCH 294/397] Add GPG.list_packets() function from drebs' wrapper code. * TODO This needs unittests. --- src/gnupg.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/gnupg.py b/src/gnupg.py index 645f40f..477877c 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1048,6 +1048,14 @@ use_agent: %s getattr(result, keyword)(L) return result + def list_packets(self, raw_data): + """List the packet contents of a file.""" + args = ["--list-packets"] + result = self._result_map['packets'](self) + self._handle_io(args, _make_binary_stream(raw_data, self.encoding), + result) + return result + def list_sigs(self, *keyids): """xxx implement me From f64b0eef03c1a8a72918d4a47c5017c91240f505 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 13:47:50 +0000 Subject: [PATCH 295/397] Add docstring for GPG.list_sigs(). --- src/gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index 477877c..bc37296 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1057,7 +1057,7 @@ use_agent: %s return result def list_sigs(self, *keyids): - """xxx implement me + """Get the signatures for each of the ``keyids``. The GnuPG option '--show-photos', according to the GnuPG manual, "does not work with --with-colons", but since we can't rely on all versions From 6cbed40f84865511e56657edf447cff1d768c6fd Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 13:49:06 +0000 Subject: [PATCH 296/397] Add doctest and docstring for GPG.list_sigs(). --- src/gnupg.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index bc37296..d87b356 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1059,10 +1059,15 @@ use_agent: %s def list_sigs(self, *keyids): """Get the signatures for each of the ``keyids``. - The GnuPG option '--show-photos', according to the GnuPG manual, "does - not work with --with-colons", but since we can't rely on all versions - of GnuPG to explicitly handle this correctly, we should probably - include it in the args. + >>> import gnupg + >>> gpg = gnupg.GPG(homedir="./tests/doctest") + >>> key_input = gpg.gen_key_input() + >>> key = gpg.gen_key(key_input) + >>> assert key.fingerprint + + :rtype: dict + :returns: A dictionary whose keys are the original keyid parameters, + and whose values are lists of signatures. """ raise NotImplemented("Functionality for '--list-sigs' not implemented.") From 406f212a3f7bea951185d6ea2574eab8db50a225 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 13:50:31 +0000 Subject: [PATCH 297/397] Remove old keyring property code. --- src/gnupg.py | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index d87b356..14d1b15 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -295,36 +295,6 @@ class GPGBase(object): os.unline(loc) log.debug("Removed binary symlink '%s'" % loc) - # @property - # def keyring(self): - # """Get the public keyring.""" - # return self._keyring - # - # @keyring.setter - # def keyring(self, pub): - # """Set the file to use as GnuPG's current (public) keyring. - # - # :param str pub: The filename, relative to :attr:``GPG.homedir``, to use - # for storing public key data. - # """ - # ring = _fix_unsafe(pub) if pub else 'pubring.gpg' - # self._keyring = os.path.join(self._homedir, ring) - # - # @property - # def secring(self): - # """Get the secret keyring.""" - # return self._secring - # - # @secring.setter - # def secring(self, sec): - # """Set the file to use as GnuPG's current secret keyring. - # - # :param str pub: The filename, relative to :attr:``GPG.homedir``, to use - # for storing secret key data. - # """ - # ring = _fix_unsafe(sec) if sec else 'secring.gpg' - # self._secring = os.path.join(self._homedir, ring) - @property def default_preference_list(self): """Get the default preference list.""" From 716d2f1d4e4abac82f4bb5c3ea1aba50056b059d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 22:47:12 +0000 Subject: [PATCH 298/397] Remove extra whitespace line. --- src/gnupg.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index 14d1b15..2791a3f 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -324,7 +324,6 @@ class GPGBase(object): """ self._prefs = 'SHA512 SHA384 SHA256 AES256 CAMELLIA256 TWOFISH ZLIB ZIP' - def _homedir_getter(self): """Get the directory currently being used as GnuPG's homedir. From 144d5f952e9a3cfc950f1e7c023f910994152724 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 23:04:55 +0000 Subject: [PATCH 299/397] Fix bug due to comparing unicode to strings in Python2.x. --- src/_parsers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/_parsers.py b/src/_parsers.py index c4862d0..a6bddcd 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -472,6 +472,11 @@ def _sanitise(*args): :returns: A string of the items in ``checked`` delimited by spaces. """ safe_option = str() + + if not _util._py3k: + if isinstance(arg, unicode): + arg = str(arg) + try: flag = _is_allowed(arg) assert flag is not None, "_check_option(): got None for flag" From 7c3e15d99ff92e9ef13d5c0fbe3d73a2518aaf1a Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 23:06:57 +0000 Subject: [PATCH 300/397] Check for basestring type in Python2.x, and type str in Python3.x. --- src/_parsers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_parsers.py b/src/_parsers.py index a6bddcd..0cd6f4a 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -484,7 +484,8 @@ def _sanitise(*args): log.warn("_check_option(): %s" % error.message) else: safe_option += (flag + " ") - if isinstance(value, str): + if (not _util._py3k and isinstance(value, basestring)) \ + or (_util._py3k and isinstance(value, str)): values = value.split(' ') for v in values: val = _fix_unsafe(v) From e5374e8200e6e92c5219231a83760e1057edbcef Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 23:07:56 +0000 Subject: [PATCH 301/397] =?UTF-8?q?Change=20util=E2=86=92=5Futil.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/_parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_parsers.py b/src/_parsers.py index 0cd6f4a..21ddc7a 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -493,7 +493,7 @@ def _sanitise(*args): if flag in ['--encrypt', '--encrypt-files', '--decrypt', '--decrypt-files', '--import', '--verify']: ## Place checks here: - if util._is_file(val): + if _util._is_file(val): safe_option += (val + " ") else: log.debug("%s not file: %s" % (flag, val)) From 83eb95dc4a1b8415e51866674676d7c4805de3cc Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Tue, 21 May 2013 23:08:31 +0000 Subject: [PATCH 302/397] Add checks on cipher/hash/digest preference strings. --- src/_parsers.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/_parsers.py b/src/_parsers.py index 21ddc7a..3662ebc 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -505,6 +505,25 @@ def _sanitise(*args): safe_option += (val + " ") else: log.debug("'%s %s' not hex." % (flag, val)) + elif flag in ['--cipher-algo', + '--personal-cipher-prefs', + '--personal-cipher-preferences']: + legit_algos = _check_preferences(val, 'cipher') + if legit_algos: + safe_option += (legit_algos + " ") + else: + log.debug("'%s' is not cipher" + % _fix_unsafe(val)) + elif flag in ['--compress-algo', + '--compression-algo', + '--personal-compress-prefs', + '--personal-compress-preferences']: + legit_algos = _check_preferences(val, 'compress') + if legit_algos: + safe_option += (legit_algos + " ") + else: + log.debug("'%s' not compress algo" + % _fix_unsafe(val)) else: safe_option += (val + " ") log.debug("_check_option(): No checks for %s" From 0b473774c1fc0fc49ca36d780693675a5d5de654 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 07:56:05 +0000 Subject: [PATCH 303/397] Remove extra whitespace from import statement in _util.py --- src/_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_util.py b/src/_util.py index d2a0ef9..261ae70 100644 --- a/src/_util.py +++ b/src/_util.py @@ -21,7 +21,7 @@ util.py Extra utilities for python-gnupg. ''' -from datetime import datetime +from datetime import datetime import os import time From 57c83acc312da10e2287ed2e87b2f137349afa85 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 07:56:57 +0000 Subject: [PATCH 304/397] Rearrange stdlib imports in _util.py to be alphabetical --- src/_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_util.py b/src/_util.py index 261ae70..7e4ae8b 100644 --- a/src/_util.py +++ b/src/_util.py @@ -25,10 +25,10 @@ from datetime import datetime import os import time +import threading import random import string import sys -import threading import _logger From 5a352972b38e656a2da2b7218e322a33a10e5ede Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 07:59:21 +0000 Subject: [PATCH 305/397] =?UTF-8?q?Add=20codec=20translation=20mapping=20f?= =?UTF-8?q?unction=20finder=20for=20unicode=E2=86=90=E2=86=92str=20convers?= =?UTF-8?q?ion.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/_util.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/_util.py b/src/_util.py index 7e4ae8b..5fb2a1e 100644 --- a/src/_util.py +++ b/src/_util.py @@ -23,6 +23,8 @@ Extra utilities for python-gnupg. from datetime import datetime +import codecs +import encodings import os import time import threading @@ -63,6 +65,40 @@ _conf = os.path.join(os.path.join(_user, '.config'), 'python-gnupg') log = _logger.create_logger(0) +def find_encodings(enc=None, system=False): + """Find functions for encoding translations for a specific codec. + + :param str enc: The codec to find translation functions for. It will be + normalized by converting to lowercase, excluding + everything which is not ascii, and hyphens will be + converted to underscores. + + :param bool system: If True, find encodings based on the system's stdin + encoding, otherwise assume utf-8. + + :raises: :exc:LookupError if the normalized codec, ``enc``, cannot be + found in Python's encoding translation map. + """ + if not enc: + enc = 'utf-8' + + if system: + if getattr(sys.stdin, 'encoding', None) is None: + enc = sys.stdin.encoding + log.debug("Obtained encoding from stdin: %s" % enc) + else: + enc = 'ascii' + + ## have to have lowercase to work, see + ## http://docs.python.org/dev/library/codecs.html#standard-encodings + enc = enc.lower() + codec_alias = encodings.normalize_encoding(enc) + + codecs.register(encodings.search_function) + coder = codecs.lookup(codec_alias) + + return coder + def _copy_data(instream, outstream): """Copy data from one stream to another. From 7d1efa81f927430df1989c77f3701671fea055e7 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 08:00:31 +0000 Subject: [PATCH 306/397] Use codec translation finder in _util._copy_data(). --- src/_util.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/_util.py b/src/_util.py index 5fb2a1e..ffffceb 100644 --- a/src/_util.py +++ b/src/_util.py @@ -116,11 +116,7 @@ def _copy_data(instream, outstream): # log.exception(ae) # return - if hasattr(sys.stdin, 'encoding'): - enc = sys.stdin.encoding - log.debug("Obtained encoding from stdin: %s" % enc) - else: - enc = 'ascii' + coder = find_encodings() while True: data = instream.read(1024) @@ -133,7 +129,7 @@ def _copy_data(instream, outstream): outstream.write(data) except UnicodeError: try: - outstream.write(data.encode(enc)) + outstream.write(coder.encode(data)) except IOError: log.exception("Error sending data: Broken pipe") break From fc190c65c26e025ff7875cc3dd2fa2130cd526e0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 08:01:25 +0000 Subject: [PATCH 307/397] Kill a superfluous log statement -- gnupg is noisy enough --- src/_util.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_util.py b/src/_util.py index ffffceb..c15265d 100644 --- a/src/_util.py +++ b/src/_util.py @@ -158,8 +158,7 @@ def _create_if_necessary(directory): directory = os.path.abspath(directory) if not os.path.isdir(directory): - log.debug("Creating directory: %s" % directory) - log.info("%s" % message) + log.info("Creating directory: %s" % directory) try: os.makedirs(directory, 0x1C0) except OSError as ose: From 3ff097499fbcedaa764b88c69f67a68aeb3f7e4d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 08:02:43 +0000 Subject: [PATCH 308/397] Fix a bug due to using inclusive-and instead of exclusive-or. --- src/_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_util.py b/src/_util.py index c15265d..f5c0c85 100644 --- a/src/_util.py +++ b/src/_util.py @@ -208,7 +208,7 @@ def _has_readwrite(path): :rtype: bool :returns: True if real uid/gid has read+write permissions, False otherwise. """ - return os.access(path, os.R_OK and os.W_OK) + return os.access(path, os.R_OK ^ os.W_OK) def _is_file(input): """Check that the size of the thing which is supposed to be a filename has From 5059b2c7b28255737099daa9b55a734d58a8467a Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 16:48:14 +0000 Subject: [PATCH 309/397] Move import statement --- tests/test_gnupg.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 2924611..32ea654 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -7,9 +7,10 @@ Copyright Ā© 2013 Isis Lovecruft. Copyright Ā© 2008-2013 Vinay Sajip. All rights reserved. """ +from functools import wraps + import argparse import doctest -from functools import wraps import io import logging import os From ac8ef6c7215485a43533a67098d2ead284fbc887 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 16:49:40 +0000 Subject: [PATCH 310/397] Add codecs and encodings to test handler so that open() uses them --- tests/test_gnupg.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 32ea654..8d7bb6b 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -10,6 +10,8 @@ Copyright Ā© 2008-2013 Vinay Sajip. All rights reserved. from functools import wraps import argparse +import codecs +import encodings import doctest import io import logging From c36687f45abc248901695cf4d58b482946f197db Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 16:50:15 +0000 Subject: [PATCH 311/397] =?UTF-8?q?util=E2=86=92=5Futil=20&=20parsers?= =?UTF-8?q?=E2=86=92=5Fparsers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 8d7bb6b..c719ea5 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -28,8 +28,8 @@ else: import unittest import gnupg -from gnupg import parsers -from gnupg import util +from gnupg import _parsers +from gnupg import _util from gnupg import _logger log = _logger.create_logger(10) From a3ac1efb942a934cfa20a39b1d7bea053310d4f6 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 16:52:35 +0000 Subject: [PATCH 312/397] Use the log object from _util in test handler. --- tests/test_gnupg.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index c719ea5..390704a 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -32,7 +32,10 @@ from gnupg import _parsers from gnupg import _util from gnupg import _logger -log = _logger.create_logger(10) + +log = _util.log +log.setLevel(9) + _here = os.path.join(os.getcwd(), 'tests') _files = os.path.join(_here, 'files') _tempd = os.path.join(_here, 'tmp') From 9b6f823a6e8f8761810c63c320012a92638d63ce Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 16:53:18 +0000 Subject: [PATCH 313/397] Whitespace fixes --- tests/test_gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 390704a..bcfeb73 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -42,9 +42,9 @@ _tempd = os.path.join(_here, 'tmp') tempfile.tempdir = _tempd if not os.path.isdir(tempfile.gettempdir()): - os.mkdir(tempfile.gettempdir()) log.debug("Creating temporary testing directory: %s" - % tempfile.gettempdir()) + % tempfile.gettempdir()) + os.makedirs(tempfile.gettempdir()) @wraps(tempfile.TemporaryFile) def _make_tempfile(*args, **kwargs): From 2d19b7d7714387154a9826899a1cf3926b3894c4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 16:54:56 +0000 Subject: [PATCH 314/397] Add a way to save generated test keys for later debugging --- tests/test_gnupg.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index bcfeb73..3d80491 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -51,6 +51,7 @@ def _make_tempfile(*args, **kwargs): return tempfile.TemporaryFile(dir=tempfile.gettempdir(), *args, **kwargs) +RETAIN_TEST_DIRS = True KEYS_TO_IMPORT = """-----BEGIN PGP PUBLIC KEY BLOCK----- @@ -137,11 +138,18 @@ class GPGTestCase(unittest.TestCase): def setUp(self): """This method is called once per self.test_* method.""" - log.warn("\r%s" % str("=" * 78)) + print "%s%s%s" % (os.linesep, str("=" * 70), os.linesep) hd = tempfile.mkdtemp() if os.path.exists(hd): - self.assertTrue(os.path.isdir(hd), "Not a directory: %s" % hd) - shutil.rmtree(hd) + if not RETAIN_TEST_DIRS: + self.assertTrue(os.path.isdir(hd), "Not a directory: %s" % hd) + shutil.rmtree(hd) + + if not os.path.exists(hd): + os.makedirs(hd) + self.assertTrue(os.path.isdir(hd), "Not a directory: %s" % hd) + + self.gpg = gnupg.GPG(binary='gpg', homedir=hd) self.homedir = hd self.gpg = gnupg.GPG(homedir=hd, binary='gpg') self.keyring = os.path.join(self.homedir, 'pubring.gpg') From e6538d6ca348c6857102b425bdb21d8349c88c15 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 16:57:41 +0000 Subject: [PATCH 315/397] Change keyring/secring attrs to be based on settings in tests --- tests/test_gnupg.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 3d80491..7aef55c 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -151,9 +151,9 @@ class GPGTestCase(unittest.TestCase): self.gpg = gnupg.GPG(binary='gpg', homedir=hd) self.homedir = hd - self.gpg = gnupg.GPG(homedir=hd, binary='gpg') - self.keyring = os.path.join(self.homedir, 'pubring.gpg') - self.secring = os.path.join(self.homedir, 'secring.gpg') + self.keyring = self.gpg.keyring + self.secring = self.gpg.secring + self.insecure_prng = False def tearDown(self): """This is called once per self.test_* method after the test run.""" From 18f0e97134f46d892b99d87e0a7095d4dd0d223f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 16:58:47 +0000 Subject: [PATCH 316/397] Remove extra whitespace --- tests/test_gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 7aef55c..34889f4 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -164,8 +164,8 @@ class GPGTestCase(unittest.TestCase): log.error(ose) else: log.warn("Can't delete homedir: '%s' not a directory" - % self.homedir) - log.warn("%s" % str("=" * 78)) + % self.homedir) + log.warn("%s%s%s" % (os.linesep, str("=" * 70), os.linesep)) def test_parsers_fix_unsafe(self): """Test that unsafe inputs are quoted out and then ignored.""" From 60d6f676e53b021ef7c99032ffdc8ac3f0491b82 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:01:36 +0000 Subject: [PATCH 317/397] =?UTF-8?q?util=E2=86=92=5Futil=20&=20parsers?= =?UTF-8?q?=E2=86=92=5Fparsers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_gnupg.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 34889f4..5d72997 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -170,7 +170,7 @@ class GPGTestCase(unittest.TestCase): def test_parsers_fix_unsafe(self): """Test that unsafe inputs are quoted out and then ignored.""" shell_input = "\"&coproc /bin/sh\"" - fixed = parsers._fix_unsafe(shell_input) + fixed = _parsers._fix_unsafe(shell_input) print fixed test_file = os.path.join(_files, 'cypherpunk_manifesto') self.assertTrue(os.path.isfile(test_file)) @@ -180,17 +180,17 @@ class GPGTestCase(unittest.TestCase): def test_parsers_is_hex_valid(self): """Test that valid hexidecimal passes the parsers._is_hex() check""" valid_hex = '0A6A58A14B5946ABDE18E207A3ADB67A2CDB8B35' - self.assertTrue(parsers._is_hex(valid_hex)) + self.assertTrue(_parsers._is_hex(valid_hex)) def test_parsers_is_hex_lowercase(self): """Test parsers._is_hex() with lowercased hexidecimal""" valid_hex = 'deadbeef15abad1dea' - self.assertTrue(parsers._is_hex(valid_hex)) + self.assertTrue(_parsers._is_hex(valid_hex)) def test_parsers_is_hex_invalid(self): """Test that invalid hexidecimal fails the parsers._is_hex() check""" invalid_hex = 'cipherpunks write code' - self.assertFalse(parsers._is_hex(invalid_hex)) + self.assertFalse(_parsers._is_hex(invalid_hex)) def test_homedir_creation(self): """Test that a homedir is created if left unspecified""" @@ -282,7 +282,7 @@ class GPGTestCase(unittest.TestCase): self.assertEqual(unicode(message), instream.getvalue()) outstream = ResultStringIO(u'result:') copied = outstream - util._copy_data(instream, outstream) + _util._copy_data(instream, outstream) self.assertTrue(outstream.readable()) self.assertTrue(outstream.closed) self.assertFalse(instream.closed) @@ -574,7 +574,7 @@ class GPGTestCase(unittest.TestCase): passphrase='johanborst') self.assertTrue(sig, "Good passphrase should succeed") try: - file = util._make_binary_stream(sig.data, self.gpg.encoding) + file = _util._make_binary_stream(sig.data, self.gpg.encoding) verified = self.gpg.verify_file(file) except UnicodeDecodeError: #happens in Python 2.6 verified = self.gpg.verify_file(io.BytesIO(sig.data)) From 9437604c719f74847685e09f283450533a49f3bc Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:05:17 +0000 Subject: [PATCH 318/397] Add list-sigs implementation --- src/_parsers.py | 3 ++- src/gnupg.py | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index 3662ebc..dcbd59d 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -337,6 +337,7 @@ def _is_allowed(input): '--armour', '--clearsign', '--detach-sign', + '--list-sigs', '--sign', '--verify', ## i/o and files @@ -499,7 +500,7 @@ def _sanitise(*args): log.debug("%s not file: %s" % (flag, val)) elif flag in ['--default-key', '--recipient', '--export', '--export-secret-keys', - '--delete-keys', + '--delete-keys', '--list-sigs', '--export-secret-subkeys',]: if _is_hex(val): safe_option += (val + " ") diff --git a/src/gnupg.py b/src/gnupg.py index 2791a3f..f97b17d 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1038,7 +1038,21 @@ use_agent: %s :returns: A dictionary whose keys are the original keyid parameters, and whose values are lists of signatures. """ - raise NotImplemented("Functionality for '--list-sigs' not implemented.") + if len(keyids) > self._batch_limit: + raise ValueError( + "List signatures is limited to %d keyids simultaneously" + % self._batch_limit) + + args = ["--with-colons", "--fixed-list-mode", "--list-sigs"] + + for key in keyids: + args.append(key) + + proc = self._open_subprocess(args) + + result = self._result_map['list'](self) + self._collect_output(proc, result, stdin=p.stdin) + return result def gen_key(self, input): """Generate a GnuPG key through batch file key generation. See From 352668b7afcfcde02ffb5cf982c4ff44da9d8fbc Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:05:46 +0000 Subject: [PATCH 319/397] Use codecs.open() in gnupg.py. --- src/gnupg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gnupg.py b/src/gnupg.py index f97b17d..0c82531 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -78,6 +78,7 @@ try: except ImportError: from cStringIO import StringIO +from codecs import open as open from pprint import pprint from psutil import process_iter from subprocess import Popen From b6797d6a477cf79ce4e30194feb6dbeb1f99c91c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:06:45 +0000 Subject: [PATCH 320/397] Replace old doctest in GPG.gen_key_input(). --- src/gnupg.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 0c82531..d5d2d77 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1059,12 +1059,11 @@ use_agent: %s """Generate a GnuPG key through batch file key generation. See :meth:`GPG.gen_key_input()` for creating the control input. - >>> gpg = GPG(homedir="keys") - >>> input = gpg.gen_key_input() - >>> result = gpg.gen_key(input) - >>> assert result - >>> result = gpg.gen_key('foo') - >>> assert not result + >>> import gnupg + >>> gpg = gnupg.GPG(homedir="./tests/doctest") + >>> key_input = gpg.gen_key_input() + >>> key = gpg.gen_key(key_input) + >>> assert key.fingerprint :param dict input: A dictionary of parameters and values for the new key. From 5578f130b84aa00b7a057ea8b00da8056c15a021 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:09:02 +0000 Subject: [PATCH 321/397] Update docs for GPG.gen_key_input(). --- src/gnupg.py | 231 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 134 insertions(+), 97 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index d5d2d77..e11e1af 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1083,43 +1083,71 @@ use_agent: %s The GnuPG batch file key generation feature allows unattended key generation by creating a file with special syntax and then providing it - to: ``gpg --gen-key --batch``: + to: ``gpg --gen-key --batch``. Batch files look like this: + Name-Real: Alice + Name-Email: alice@inter.net + Expire-Date: 2014-04-01 Key-Type: RSA Key-Length: 4096 - Name-Real: Autogenerated Key - Name-Email: %s@%s - Expire-Date: 2014-04-01 + Key-Usage: cert + Subkey-Type: RSA + Subkey-Length: 4096 + Subkey-Usage: encrypt,sign,auth + Passphrase: sekrit %pubring foo.gpg %secring sec.gpg %commit - Key-Type: DSA - Key-Length: 1024 - Subkey-Type: ELG-E - Subkey-Length: 1024 - Name-Real: Joe Tester - Name-Comment: with stupid passphrase - Name-Email: joe@foo.bar - Expire-Date: 0 - Passphrase: abc - %pubring foo.pub - %secring foo.sec - %commit + which is what this function creates for you. All of the available, + non-control parameters are detailed below (control parameters are the + ones which begin with a '%'). For example, to generate the batch file + example above, use like this: - >>> gpg = GPG(homedir="keys") - >>> params = {'name_real':'python-gnupg tester', 'name_email':'test@ing'} - >>> key_input = gpg.gen_key_input(**params) - >>> result = gpg.gen_key(input) - >>> assert result + >>> import gnupg + GnuPG logging disabled... + >>> from __future__ import print_function + >>> gpg = gnupg.GPG(homedir='./tests/doctest') + >>> alice = { 'name_real': 'Alice', + ... 'name_email': 'alice@inter.net', + ... 'expire_date': '2014-04-01', + ... 'key_type': 'RSA', + ... 'key_length': 4096, + ... 'key_usage': '', + ... 'subkey_type': 'RSA', + ... 'subkey_length': 4096, + ... 'subkey_usage': 'encrypt,sign,auth', + ... 'passphrase': 'sekrit'} + >>> alice_input = gpg.gen_key_input(**alice) + >>> print(alice_input) + Key-Type: RSA + Subkey-Type: RSA + Subkey-Usage: encrypt,sign,auth + Expire-Date: 2014-04-01 + Passphrase: sekrit + Name-Real: Alice + Name-Email: alice@inter.net + Key-Length: 4096 + Subkey-Length: 4096 + %pubring ./tests/doctest/pubring.gpg + %secring ./tests/doctest/secring.gpg + %commit + + >>> alice_key = gpg.gen_key(alice_input) + >>> assert alice_key is not None + >>> assert alice_key.fingerprint is not None + >>> message = "no one else can read my sekrit message" + >>> encrypted = gpg.encrypt(message, alice_key.fingerprint) + >>> assert isinstance(encrypted.data, str) :param bool testing: Uses a faster, albeit insecure random number - generator to create keys. This should only be used - for testing purposes, for keys which are going to - be created and then soon after destroyed, and - never for the generation of actual use keys. + generator to create keys. This should only be + used for testing purposes, for keys which are + going to be created and then soon after + destroyed, and never for the generation of actual + use keys. - :param str name_real: The name portion of the UID of 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_email: The email in the UID of the generated key. (default: $USER@$(hostname) ) Remember to use @@ -1129,38 +1157,39 @@ use_agent: %s :param:`name_email ` must be provided, or else no user ID is created. - :param str key_type: One of 'RSA', 'DSA', or 'ELG-E'. (default: 'RSA') - Starts a new parameter block by giving the type of - the primary key. The algorithm must be capable of - signing. This is a required parameter. The - algorithm may either be an OpenPGP algorithm - number or a string with the algorithm name. The - special value ā€˜default’ may be used for algo to - create the default key type; in this case a - :param:`key_usage ` should not be given - and ā€˜default’ must also be used for - :param:`subkey_type `. + :param str key_type: One of 'RSA', 'DSA', 'ELG-E', or 'default'. + (default: 'default') Starts a new parameter block + by giving the type of the primary key. The + algorithm must be capable of signing. This is a + required parameter. The algorithm may either be + an OpenPGP algorithm number or a string with the + algorithm name. The special value ā€˜default’ may + be used for algo to create the default key type; + in this case a :param:`key_usage ` + should not be given and ā€˜default’ must also be + used for :param:`subkey_type `. :param int key_length: The requested length of the generated key in bits. (Default: 4096) :param str key_grip: hexstring This is an optional hexidecimal string - which is used to generate a CSR or certificate for - an already existing key. :param:key_length + which is used to generate a CSR or certificate + for an already existing key. :param:key_length will be ignored if this parameter is given. :param str key_usage: Space or comma delimited string of key - usages. Allowed values are ā€˜encrypt’, ā€˜sign’, and - ā€˜auth’. This is used to generate the key + usages. Allowed values are ā€˜encrypt’, ā€˜sign’, + and ā€˜auth’. This is used to generate the key flags. Please make sure that the algorithm is - capable of this usage. Note that OpenPGP requires - that all primary keys are capable of + capable of this usage. Note that OpenPGP + requires that all primary keys are capable of certification, so no matter what usage is given here, the ā€˜cert’ flag will be on. If no ā€˜Key-Usage’ is specified and the ā€˜Key-Type’ is not ā€˜default’, all allowed usages for that - particular algorithm are used; if it is not given - but ā€˜default’ is used the usage will be ā€˜sign’. + particular algorithm are used; if it is not + given but ā€˜default’ is used the usage will be + ā€˜sign’. :param str subkey_type: This generates a secondary key (subkey). Currently only one subkey can be @@ -1182,18 +1211,18 @@ use_agent: %s assumed. Note that there is no check done on the overflow of the type used by OpenPGP for timestamps. Thus you better make sure that the - given value make sense. Although OpenPGP works with - time intervals, GnuPG uses an absolute value + given value make sense. Although OpenPGP works + with time intervals, GnuPG uses an absolute value internally and thus the last year we can represent is 2105. - :param str creation_date: Set the creation date of the key as stored in - the key information and which is also part of - the fingerprint calculation. Either a date - like "1986-04-26" or a full timestamp like - "19860426T042640" may be used. The time is - considered to be UTC. If it is not given the - current time is used. + :param str creation_date: Set the creation date of the key as stored + in the key information and which is also + part of the fingerprint calculation. Either + a date like "1986-04-26" or a full timestamp + like "19860426T042640" may be used. The time + is considered to be UTC. If it is not given + the current time is used. :param str passphrase: The passphrase for the new key. The default is to not use any passphrase. Note that @@ -1201,10 +1230,11 @@ use_agent: %s passphrase for batch key generation -- GnuPG will ignore the ``passphrase`` parameter, stop, and ask the user for the new passphrase. - However, we can put the command '%no-protection' - into the batch key generation file to allow a - passwordless key to be created, which can then - have its passphrase set later with '--edit-key'. + However, we can put the command + '%no-protection' into the batch key generation + file to allow a passwordless key to be created, + which can then have its passphrase set later + with '--edit-key'. :param str preferences: Set the cipher, hash, and compression preference values for this key. This expects @@ -1212,58 +1242,70 @@ use_agent: %s ā€˜setpref’ in the --edit-key menu. :param str revoker: Should be given as 'algo:fpr' [case sensitive]. - 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.) fpr is the - fingerprint of the designated revoker. The optional - ā€˜sensitive’ flag marks the designated revoker as - sensitive information. Only v4 keys may be - designated revokers. + 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.) fpr + is the fingerprint of the designated revoker. The + optional ā€˜sensitive’ flag marks the designated + revoker as sensitive information. Only v4 keys may + be designated revokers. :param str keyserver: This is an optional parameter that specifies the preferred keyserver URL for the key. :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 100 characters and should not contain - spaces. It is useful for batch key generation to - associate a key parameter block with a status line. + status lines KEY_CREATED and + KEY_NOT_CREATED. string may be up to 100 characters + and should not contain spaces. It is useful for + batch key generation to associate a key parameter + block with a status line. :rtype: str - :returns: A suitable input string for the ``GPG.gen_key()`` method, the - latter of which will create the new keypair. + :returns: A suitable input string for the ``GPG.gen_key()`` method, + the latter of which will create the new keypair. - see http://www.gnupg.org/documentation/manuals/gnupg-devel/Unattended-GPG-key-generation.html#Unattended-GPG-key-generation + see http://www.gnupg.org/documentation/manuals/gnupg-devel/Unatten\ + ded-GPG-key-generation.html#Unattended-GPG-key-generation for more details. """ parms = {} - for key, val in list(kwargs.items()): - key = key.replace('_','-').title() - if str(val).strip(): # skip empty strings - parms[key] = val - - parms.setdefault('Key-Type', 'RSA') + parms.setdefault('Key-Type', 'default') parms.setdefault('Key-Length', 4096) parms.setdefault('Name-Real', "Autogenerated Key") parms.setdefault('Expire-Date', _util._next_year()) - try: - logname = os.environ['LOGNAME'] - except KeyError: - logname = os.environ['USERNAME'] + try: logname = os.environ['LOGNAME'] + except KeyError: logname = os.environ['USERNAME'] hostname = socket.gethostname() - - parms.setdefault('Name-Email', "%s@%s" % (logname.replace(' ', '_'), - hostname)) + parms.setdefault('Name-Email', "%s@%s" + % (logname.replace(' ', '_'), hostname)) if testing: ## This specific comment string is required by (some? all?) ## versions of GnuPG to use the insecure PRNG: parms.setdefault('Name-Comment', 'insecure!') - out = "Key-Type: %s\n" % parms.pop('Key-Type') + for key, val in list(kwargs.items()): + key = key.replace('_','-').title() + ## to set 'cert', 'Key-Usage' must be blank string + if not key in ('Key-Usage', 'Subkey-Usage'): + ## otherwise skip anything else that's empty + if str(val).strip(): + parms[key] = val + + ## Key-Type must come first, followed by length + out = "Key-Type: %s\n" % parms.pop('Key-Type') + out += "Key-Length: %d\n" % parms.pop('Key-Length') + if 'Subkey-Type' in parms.keys(): + out += "Subkey-Type: %s\n" % parms.pop('Subkey-Type') + if 'Subkey-Length' in parms.keys(): + out += "Subkey-Length: %s\n" % parms.pop('Subkey-Length') + + if 'Name-Real' in parms.items(): + ## xxx für die Dateinamen + name = real_name.lower().replace(' ', '') for key, val in list(parms.items()): out += "%s: %s\n" % (key, val) @@ -1281,26 +1323,22 @@ use_agent: %s out += "%commit\n" return out - # - # ENCRYPTION - # - def encrypt_file(self, file, recipients, sign_with=None, - always_trust=False, passphrase=None, armor=True, + def encrypt_file(self, filename, recipients, default_key=None, + always_trust=True, passphrase=None, armor=True, output=None, encrypt=True, symmetric=False, cipher_algo='AES256', digest_algo='SHA512', compress_algo='ZLIB'): """Encrypt the message read from ``file``. - :type file: file or :class:BytesIO - :param file: The file or bytestream to encrypt. + :param str filename: The file or bytestream to encrypt. :type recipients: str or list or tuple :param recipients: The recipients to encrypt to. Recipients may be specified by UID or keyID/fingerprint. - :param str sign_with: The keyID to use for signing, i.e. - "gpg --sign --default-key A3ADB67A2CDB8B35 ..." + :param str default_key: The keyID to use for signing, i.e. + "gpg --default-key A3ADB67A2CDB8B35 --sign ..." :param bool always_trust: If True, ignore trust warnings on recipient keys. If False, display trust warnings. - (default: False) + (default: True) :param bool passphrase: If True, use the stored passphrase for our secret key. @@ -1311,7 +1349,6 @@ use_agent: %s :param str output: The output file to write to. If not specified, the encrypted output is returned, and thus should be stored as an object in Python. For example: - """ args = list() From c577fdcfe5c24e03f8fea414a5554271c3460524 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:12:27 +0000 Subject: [PATCH 322/397] Add unittests for codec translator utilities. --- Makefile | 2 +- tests/test_gnupg.py | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a6a7b13..8554c62 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ cleantest: cleanup-src cleanup-tests cleanup-build rm *.log test: cleantest - python tests/test_gnupg.py parsers basic genkey sign listkeys crypt keyrings import + python tests/test_gnupg.py parsers basic encodings genkey sign listkeys crypt keyrings import install: python setup.py install --record installed-files.txt diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 5d72997..7d32436 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -192,6 +192,29 @@ class GPGTestCase(unittest.TestCase): invalid_hex = 'cipherpunks write code' self.assertFalse(_parsers._is_hex(invalid_hex)) + def test_encodings_spiteful(self): + """Test that a non-existent codec raises a LookupError.""" + enc = '#!@& dealing with unicode in Python2' + with self.assertRaises(LookupError): + _util.find_encodings(enc) + + def test_encodings_iso_8859_1(self): + """Test that _util.find_encodings works for Chinese Traditional.""" + enc = 'big5' + coder = _util.find_encodings(enc) + msg = u'å…‰ę¦®ēš„äø­åœ‹äŗŗę°‘ę‡‰č©²ę‘§ęÆ€äø­åœ‹é•·åŸŽé˜²ē«ē‰†ć€‚' + encoded = coder.encode(msg)[0] + decoded = coder.decode(encoded)[0] + self.assertEqual(msg, decoded) + + def test_encodings_non_specified(self): + """Test that using the default utf-8 encoding works.""" + coder = _util.find_encodings() + msg = u'Nutella Ć” brauư mitt, smear þaư þykkur!' + encoded = coder.encode(msg)[0] + decoded = coder.decode(encoded)[0] + self.assertEqual(msg, decoded) + def test_homedir_creation(self): """Test that a homedir is created if left unspecified""" gpg = gnupg.GPG(binary='gpg') @@ -773,6 +796,10 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_parsers_is_hex_lowercase', 'test_parsers_is_hex_invalid', 'test_copy_data_bytesio',]), + 'encodings': set(['test_encodings_iso_8859_1', + 'test_encodings_spiteful', + 'test_encodings_non_specified', + ]), 'basic': set(['test_homedir_creation', 'test_binary_discovery', 'test_gpg_binary', From e563faa344dc39c0f85f2a4b9dc61be65c1e8a8c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:17:02 +0000 Subject: [PATCH 323/397] Add print() in unittest for the binary location to be sure PATH hack works. --- tests/test_gnupg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 7d32436..8de3344 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -261,6 +261,7 @@ class GPGTestCase(unittest.TestCase): def test_gpg_binary_not_abs(self): """Test that a non-absolute path to gpg results in a full path.""" + print self.gpg.binary self.assertTrue(os.path.isabs(self.gpg.binary)) def test_make_args_drop_protected_options(self): From 8cdc85aa0e85daf5d04cf95779ff46ad6c5493f3 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:18:58 +0000 Subject: [PATCH 324/397] Add some point the order of these two args was switch, so test should match. --- tests/test_gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 8de3344..32c4269 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -269,11 +269,11 @@ class GPGTestCase(unittest.TestCase): self.gpg.options = ['--tyrannosaurus-rex', '--stegosaurus'] cmd = self.gpg._make_args(None, False) expected = ['/usr/bin/gpg', - '--status-fd 2 --no-tty --no-emit-version', + '--no-emit-version --no-tty --status-fd 2', '--homedir "%s"' % self.homedir, '--no-default-keyring --keyring %s' % self.keyring, '--secret-keyring %s' % self.secring, - '--no-use-agent',] + '--no-use-agent'] self.assertListEqual(cmd, expected) def test_make_args(self): From fba19da0a0de7eba291ba07d3f6e4c03448472ac Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:20:24 +0000 Subject: [PATCH 325/397] Use the setting in GPGTestCase.__init__() for insecure PRNG. --- tests/test_gnupg.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 32c4269..9780ed0 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -333,7 +333,7 @@ class GPGTestCase(unittest.TestCase): batch['Subkey-Type'] = subkey_type batch['Subkey-Length'] = key_length - key_input = self.gpg.gen_key_input(testing=True, **batch) + key_input = self.gpg.gen_key_input(testing=self.insecure_prng, **batch) return key_input def generate_key(self, real_name, email_domain, **kwargs): @@ -586,9 +586,13 @@ class GPGTestCase(unittest.TestCase): "Fingerprints must match") self.assertEqual(verified.status, 'signature valid') self.assertAlmostEqual(int(now), int(verified.timestamp), delta=1000) - self.assertEqual( - verified.username, - u'Bruce Schneier (insecure!) ') + if self.insecure_prng: + self.assertEqual( + verified.username, + u'Bruce Schneier (insecure!) ') + else: + self.assertEqual(verified.username, + u'Bruce Schneier ') def test_signature_verification_clearsign(self): """Test verfication of an embedded signature.""" From 3780a6bed208317f3c397f8e154bb8a386beb5e1 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:22:30 +0000 Subject: [PATCH 326/397] More whitespace/style fixes. --- tests/test_gnupg.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 9780ed0..7c63504 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -607,17 +607,15 @@ class GPGTestCase(unittest.TestCase): except UnicodeDecodeError: #happens in Python 2.6 verified = self.gpg.verify_file(io.BytesIO(sig.data)) if key.fingerprint != verified.fingerprint: - logger.debug("key: %r", key.fingerprint) - logger.debug("ver: %r", verified.fingerprint) - self.assertEqual(key.fingerprint, verified.fingerprint, - "Fingerprints must match") + log.warn("key fingerprint: %r", key.fingerprint) + log.warn("verified fingerprint: %r", verified.fingerprint) + self.assertEqual(key.fingerprint, verified.fingerprint) def test_signature_verification_detached(self): """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 manifesto: - sig = self.gpg.sign(manifesto, keyid=key.fingerprint, + with open(os.path.join(_files, 'cypherpunk_manifesto'), 'rb') as cm: + sig = self.gpg.sign(cm, keyid=key.fingerprint, passphrase='paulos.l.m.barreto', detach=True, clearsign=False) self.assertTrue(sig.data, "File signing should succeed") @@ -626,21 +624,21 @@ class GPGTestCase(unittest.TestCase): sigfile.write(sig.data) sigfile.seek(0) - verified = self.gpg.verify_file(manifesto, sigfilename) + verified = self.gpg.verify_file(cm, sigfilename) if key.fingerprint != verified.fingerprint: - logger.debug("key: %r", key.fingerprint) - logger.debug("ver: %r", verified.fingerprint) + log.warn("key fingerprint: %r", key.fingerprint) + log.warn("verified fingerprint: %r", verified.fingerprint) + self.assertEqual(key.fingerprint, verified.fingerprint) - self.assertEqual(key.fingerprint, verified.fingerprint, - "Fingerprints must match") + if os.path.isfile(sigfilename): + os.unlink(sigfilename) def test_signature_verification_detached_binary(self): """Test that detached signature verification in binary mode fails.""" key = self.generate_key("Adi Shamir", "rsa.com") - with open(os.path.join(_files, 'cypherpunk_manifesto'), - 'rb') as manifesto: - sig = self.gpg.sign(manifesto, keyid=key.fingerprint, + with open(os.path.join(_files, 'cypherpunk_manifesto'), 'rb') as cm: + sig = self.gpg.sign(cm, keyid=key.fingerprint, passphrase='adishamir', detach=True, binary=True, clearsign=False) self.assertTrue(sig.data, "File signing should succeed") From ba37ce083d4594840edcf3020f856f9e9b18edd9 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:23:18 +0000 Subject: [PATCH 327/397] Explicitly set the passphrase on one of the unittest keys. --- tests/test_gnupg.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 7c63504..f6563a1 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -658,11 +658,12 @@ class GPGTestCase(unittest.TestCase): log.debug("test_deletion ends") def test_encryption(self): - """Test encryption of a message string.""" + """Test encryption of a message string""" key = self.generate_key("Craig Gentry", "xorr.ox", passphrase="craiggentry") gentry = key.fingerprint - key = self.generate_key("Marten van Dijk", "xorr.ox") + key = self.generate_key("Marten van Dijk", "xorr.ox", + passphrase="martenvandijk") dijk = key.fingerprint gpg = self.gpg message = """ @@ -672,8 +673,10 @@ transparent multi-hop wireless backhauls that are able to perform statistical analysis of different kinds of data (temperature, humidity, etc.) coming from a WSN while ensuring both end-to-end encryption and hop-by-hop authentication.""" - encrypted = str(gpg.encrypt(message, dijk)) - self.assertNotEqual(message, encrypted, "Data must have changed") + encrypted = str(gpg.encrypt(message, [dijk], )) + self.assertNotEqual(message, encrypted) + self.assertNotEqual(encrypted, '') + self.assertGreater(len(encrypted), 0) def test_encryption_alt_encoding(self): """Test encryption with latin-1 encoding""" From 345ca75e85e8ce8bc182aed909dccd7700983c5a Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:25:21 +0000 Subject: [PATCH 328/397] Minor fixes to encryption with iso-8859-1 encodings test. --- tests/test_gnupg.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index f6563a1..654c077 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -685,15 +685,16 @@ authentication.""" gentry = key.fingerprint key = self.generate_key("Marten van Dijk", "xorr.ox") dijk = key.fingerprint - gpg = self.gpg - gpg.encoding = 'latin-1' - if util._py3k: + self.gpg.encoding = 'latin-1' + if _util._py3k: data = 'Hello, AndrĆ©!' else: - data = unicode('Hello, AndrĆ©', gpg.encoding) - data = data.encode(gpg.encoding) - encrypted = str(gpg.encrypt(data, gentry)) - self.assertNotEqual(data, encrypted, "Data must have changed") + data = unicode('Hello, AndrĆ©', self.gpg.encoding) + data = data.encode(self.gpg.encoding) + encrypted = str(self.gpg.encrypt(data, [gentry])) + self.assertNotEqual(data, encrypted) + self.assertNotEqual(encrypted, '') + self.assertGreater(len(encrypted), 0) def test_encryption_multi_recipient(self): """Test encrypting a message for multiple recipients""" From 10014d540199a38237368df2e37976da8175f1b6 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:25:57 +0000 Subject: [PATCH 329/397] Rewrite multi-recipient encryption test --- tests/test_gnupg.py | 164 ++++++++++++++++++++++++++------------------ 1 file changed, 97 insertions(+), 67 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 654c077..0a13394 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -698,11 +698,45 @@ authentication.""" def test_encryption_multi_recipient(self): """Test encrypting a message for multiple recipients""" - key = self.generate_key("Craig Gentry", "xorr.ox", - passphrase="craiggentry") - gentry = key.fingerprint - key = self.generate_key("Marten van Dijk", "xorr.ox") - dijk = key.fingerprint + ian = { 'name_real': 'Ian Goldberg', + 'name_email': 'gold@stein', + 'key_type': 'RSA', + 'key_usage': '', + 'subkey_type': 'RSA', + 'subkey_usage': 'encrypt,sign', + 'passphrase': 'victorygin' } + kat = { 'name_real': 'Kat Hannah', + 'name_email': 'kat@pics', + 'key_type': 'RSA', + 'key_usage': '', + 'subkey_type': 'RSA', + 'subkey_usage': 'encrypt,sign', + 'passphrase': 'overalls' } + + ian_input = self.gpg.gen_key_input(**ian) + kat_input = self.gpg.gen_key_input(**kat) + + ian_key = self.gpg.gen_key(ian_input) + kat_key = self.gpg.gen_key(kat_input) + + log.debug("ian_key.status=%s" % ian_key.status) + log.debug("kat_key.status=%s" % kat_key.status) + + import pdb + pdb.set_trace() + + #self.assertIsNotNone(str(ian_key.fingerprint)) + #self.assertIsNotNone(str(kat_key.fingerprint)) + + #self.assertTrue(ian_key.primary_created) + #self.assertTrue(ian_key.subkey_created) + + #self.assertTrue(kat_key.primary_created) + #self.assertTrue(kat_key.subkey_created) + + ian_fpr = ian_key.fingerprint + kat_fpr = kat_key.fingerprint + gpg = self.gpg message = """ In 2010 Riggio and Sicari presented a practical application of homomorphic @@ -711,18 +745,23 @@ transparent multi-hop wireless backhauls that are able to perform statistical analysis of different kinds of data (temperature, humidity, etc.) coming from a WSN while ensuring both end-to-end encryption and hop-by-hop authentication.""" - encrypted2 = str(gpg.encrypt(message, [gentry, dijk])) - self.assertNotEqual(message, encrypted2, "PT and CT should not match") + encrypted = gpg.encrypt(message, [ian_fpr, kat_fpr]) + self.assertNotEqual(message, str(encrypted.data)) + self.assertNotEqual(str(encrypted.data), '') + self.assertGreater(len(str(encrypted.data)), 0) def test_decryption(self): """Test decryption""" - key = self.generate_key("Frey", "fr.ey", - passphrase="craiggentry") - frey = key.fingerprint - key = self.generate_key("Rück", "rü.ck", - passphrase="ruck") - ruck = key.fingerprint - gpg = self.gpg + key = self.generate_key("Frey", "fr.ey", passphrase="frey") + frey_fpr = key.fingerprint + frey = self.gpg.export_keys(key.fingerprint) + self.gpg.import_keys(frey) + + key = self.generate_key("Rück", "rü.ck", passphrase="ruck") + ruck_fpr = key.fingerprint + ruck = self.gpg.export_keys(key.fingerprint) + self.gpg.import_keys(ruck) + message = """ In 2010 Riggio and Sicari presented a practical application of homomorphic encryption to a hybrid wireless sensor/mesh network. The system enables @@ -730,11 +769,14 @@ transparent multi-hop wireless backhauls that are able to perform statistical analysis of different kinds of data (temperature, humidity, etc.) coming from a WSN while ensuring both end-to-end encryption and hop-by-hop authentication.""" - encrypted = str(self.gpg.encrypt(message, ruck)) - decrypted = self.gpg.decrypt(encrypted, passphrase="ruck") + + encrypted = self.gpg.encrypt(message, [ruck_fpr,]) + decrypted = self.gpg.decrypt(encrypted.data, passphrase="ruck") + if message != decrypted.data: log.debug("was: %r", message) log.debug("new: %r", decrypted.data) + self.assertEqual(message, decrypted.data) def test_decryption_multi_recipient(self): @@ -742,7 +784,8 @@ authentication.""" key = self.generate_key("Craig Gentry", "xorr.ox", passphrase="craiggentry") gentry = key.fingerprint - key = self.generate_key("Marten van Dijk", "xorr.ox") + key = self.generate_key("Marten van Dijk", "xorr.ox", + passphrase="martenvandijk") dijk = key.fingerprint message = """ In 2010 Riggio and Sicari presented a practical application of homomorphic @@ -752,7 +795,7 @@ analysis of different kinds of data (temperature, humidity, etc.) coming from a WSN while ensuring both end-to-end encryption and hop-by-hop authentication.""" encrypted = str(self.gpg.encrypt(message, [gentry, dijk])) - self.assertNotEqual(message, encrypted, "PT and CT should not match") + self.assertNotEqual(message, encrypted) decrypted1 = self.gpg.decrypt(encrypted, passphrase="craiggentry") self.assertEqual(message, str(decrypted1.data)) decrypted2 = self.gpg.decrypt(encrypted, passphrase="martenvandijk") @@ -760,42 +803,40 @@ authentication.""" def test_symmetric_encryption_and_decryption(self): """Test symmetric encryption and decryption""" - msg = """ -If you have something that you don't want anyone to know, maybe you shouldn't -be doing it in the first place. - Eric Schmidt, CEO of Google""" + msg = """If you have something that you don't want anyone to know, + maybe you shouldn't be doing it in the first place. - Eric Schmidt, CEO + of Google""" encrypted = str(self.gpg.encrypt(msg, None, passphrase='quiscustodiet', - symmetric=True)) + symmetric=True, encrypt=False)) decrypted = self.gpg.decrypt(encrypted, passphrase='quiscustodiet') self.assertEqual(msg, str(decrypted.data)) def test_file_encryption_and_decryption(self): """Test that encryption/decryption to/from file works.""" - encfname = _make_tempfile() - log.debug('Created tempfile for encrypted content: %s' % encfname) - decfname = _make_tempfile() - log.debug('Created tempfile for decrypted content: f%s' % decfname) - # On Windows, if the handles aren't closed, the files can't be deleted - #os.close(encfno) - #os.close(decfno) key = self.generate_key("Andrew Able", "alpha.com", - passphrase="andy") + passphrase="andrewable") andrew = key.fingerprint - key = self.generate_key("Barbara Brown", "beta.com") + key = self.generate_key("Barbara Brown", "beta.com", + passphrase="barbarabrown") barbara = key.fingerprint + enc_outf = file(os.path.join(self.gpg.homedir, 'to-b.gpg'), 'w+') + dec_outf = file(os.path.join(self.gpg.homedir, 'to-b.txt'), 'w+') data = "Hello, world!" - file = util._make_binary_stream(data, self.gpg.encoding) - edata = self.gpg.encrypt_file(file, barbara, - armor=False, output=encfname) - ddata = self.gpg.decrypt_file(efile, passphrase="bbrown", - output=decfname) - encfname.seek(0, 0) # can't use os.SEEK_SET in 2.4 - edata = encfname.read() - ddata = decfname.read() + data_file = _util._make_binary_stream(data, self.gpg.encoding) + edata = self.gpg.encrypt_file(data_file, barbara, + armor=False, + output=enc_outf) + ddata = self.gpg.decrypt_file(enc_outf, passphrase="barbarabrown", + output=dec_outf) + enc_outf.seek(0, 0) # can't use os.SEEK_SET in 2.4 + dec_outf.seek(0, 0) + enc_data = enc_outf.read() + dec_data = dec_outf.read() data = data.encode(self.gpg.encoding) if ddata != data: log.debug("was: %r", data) - log.debug("new: %r", ddata) - self.assertEqual(data, ddata) + log.debug("new: %r", dec_data) + self.assertEqual(data, dec_data) suites = { 'parsers': set(['test_parsers_fix_unsafe', @@ -851,16 +892,10 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_deletion']), 'import': set(['test_import_only']), } -def _init_logging(): - now = datetime.now().strftime("%Y-%m-%d_%H:%M:%S") - logging.basicConfig( - filename=os.path.join(_here, "%s_test_gnupg.log" % now), - filemode="a", - format="%(asctime)s %(levelname)-5s %(name)-7s %(threadName)-10s %(message)s") - def main(args): if not args.quiet: - _init_logging() + log = _logger.create_logger(9) + log.setLevel(9) loader = unittest.TestLoader() @@ -890,6 +925,8 @@ def main(args): prog = unittest.TestProgram prog.createTests = _createTests + import pdb + pdb.set_trace() program = prog(module=GPGTestCase, testRunner=runner, testLoader=loader, @@ -897,8 +934,10 @@ def main(args): catchbreak=True) ## Finally, remove our testing directory: - if os.path.isdir(_tempd): - os.unlink(_tempd) + if not RETAIN_TEST_DIRS: + if os.path.isdir(_tempd): + shutil.rmtree(_tempd) + if __name__ == "__main__": @@ -908,25 +947,16 @@ if __name__ == "__main__": setattr(GPGTestCase, name, list(methodset)) parser = argparse.ArgumentParser(description="Unittests for python-gnupg") - parser.add_argument('--doctest', - dest='run_doctest', - type=bool, - default=False, + parser.add_argument('--doctest', dest='run_doctest', + type=bool, default=False, help='Run example code in docstrings') - parser.add_argument('--quiet', - dest='quiet', - type=bool, - default=False, + parser.add_argument('--quiet', dest='quiet', + type=bool, default=False, help='Disable logging to stdout') - parser.add_argument('--verbose', - dest='verbose', - type=int, - default=4, + parser.add_argument('--verbose', dest='verbose', + type=int, default=4, help='Set verbosity level (low=1 high=5) (default: 4)') - parser.add_argument('test', - metavar='test', - nargs='+', - type=str, + parser.add_argument('test', metavar='test', nargs='+', type=str, help='Select a test suite to run (default: all)') parser.epilog = "Available test suites: %s" % " ".join(suite_names) From 88352ef95ce051f20784c3aa72d425fbfb75aba6 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:28:34 +0000 Subject: [PATCH 330/397] Remove extra Verify and Crypt from _parsers. --- src/_parsers.py | 178 ++---------------------------------------------- 1 file changed, 7 insertions(+), 171 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index dcbd59d..d32a87c 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -621,180 +621,16 @@ def _sanitise_list(arg_list): yield safe_arg -class Verify(object): - """Parser for internal status messages from GnuPG for ``--verify``. - """ - - TRUST_UNDEFINED = 0 - TRUST_NEVER = 1 - TRUST_MARGINAL = 2 - TRUST_FULLY = 3 - TRUST_ULTIMATE = 4 - - TRUST_LEVELS = { "TRUST_UNDEFINED" : TRUST_UNDEFINED, - "TRUST_NEVER" : TRUST_NEVER, - "TRUST_MARGINAL" : TRUST_MARGINAL, - "TRUST_FULLY" : TRUST_FULLY, - "TRUST_ULTIMATE" : TRUST_ULTIMATE, } - - #: True if the signature is valid, False otherwise. - valid = False - #: A string describing the status of the signature verification. - #: Can be one of ``'signature bad'``, ``'signature good'``, - #: ``'signature valid'``, ``'signature error'``, ``'decryption failed'``, - #: ``'no public key'``, ``'key exp'``, or ``'key rev'``. - status = None - #: The fingerprint of the signing keyid. - fingerprint = None - #: The fingerprint of the corresponding public key, which may be different - #: if the signature was created with a subkey. - pubkey_fingerprint = None - #: The keyid of the signing key. - key_id = None - #: The id of the signature itself. - signature_id = None - #: The creation date of the signing key. - creation_date = None - #: The timestamp of the purported signature, if we are unable to parse it. - timestamp = None - #: The userid of the signing key which was used to create the signature. - username = None - #: When the signing key is due to expire. - expire_timestamp = None - #: The timestamp for when the signature was created. - sig_timestamp = None - #: A number 0-4 describing the trust level of the signature. - trust_level = None - #: The string corresponding to the ``trust_level`` number. - trust_text = None - - def __init__(self, gpg): - self.gpg = gpg - - def __nonzero__(self): - return self.valid - __bool__ = __nonzero__ - - def _handle_status(self, key, value): - if key in self.TRUST_LEVELS: - self.trust_text = key - self.trust_level = self.TRUST_LEVELS[key] - elif key in ("RSA_OR_IDEA", "NODATA", "IMPORT_RES", "PLAINTEXT", - "PLAINTEXT_LENGTH", "POLICY_URL", "DECRYPTION_INFO", - "DECRYPTION_OKAY", "INV_SGNR"): - pass - elif key == "BADSIG": - self.valid = False - self.status = 'signature bad' - self.key_id, self.username = value.split(None, 1) - elif key == "GOODSIG": - self.valid = True - self.status = 'signature good' - self.key_id, self.username = value.split(None, 1) - elif key == "VALIDSIG": - (self.fingerprint, - self.creation_date, - self.sig_timestamp, - self.expire_timestamp) = value.split()[:4] - self.pubkey_fingerprint = value.split()[-1] - self.status = 'signature valid' - elif key == "SIG_ID": - (self.signature_id, - self.creation_date, self.timestamp) = value.split() - elif key == "ERRSIG": - self.valid = False - (self.key_id, - algo, hash_algo, - cls, - self.timestamp) = value.split()[:5] - self.status = 'signature error' - elif key == "DECRYPTION_FAILED": - self.valid = False - self.key_id = value - self.status = 'decryption failed' - elif key == "NO_PUBKEY": - self.valid = False - self.key_id = value - self.status = 'no public key' - elif key in ("KEYEXPIRED", "SIGEXPIRED"): - # these are useless in verify, since they are spit out for any - # pub/subkeys on the key, not just the one doing the signing. - # if we want to check for signatures with expired key, - # the relevant flag is EXPKEYSIG. - pass - elif key in ("EXPKEYSIG", "REVKEYSIG"): - # signed with expired or revoked key - self.valid = False - self.key_id = value.split()[0] - self.status = (('%s %s') % (key[:3], key[3:])).lower() - else: - raise ValueError("Unknown status message: %r" % key) - - -class Crypt(Verify): - """Parser for internal status messages from GnuPG for - ``--encrypt````--decrypt``, and ``--decrypt-files``. - """ - def __init__(self, gpg): - self.gpg = gpg - self.data = str() - self.ok = False - self.status = str() - - def __nonzero__(self): - if self.ok: return True - return False - - __bool__ = __nonzero__ - - def __str__(self): - return self.data.decode(self.gpg.encoding, self.gpg._decode_errors) - - def __repr__(self): - return str(self) - - def _handle_status(self, key, value): - """Parse a status code from the attached GnuPG process. - - :raises: :exc:`ValueError` if the status message is unknown. - """ - if key in ("ENC_TO", "USERID_HINT", "GOODMDC", "END_DECRYPTION", - "BEGIN_SIGNING", "NO_SECKEY", "ERROR", "NODATA", - "CARDCTRL"): - # in the case of ERROR, this is because a more specific error - # message will have come first - pass - elif key in ("NEED_PASSPHRASE", "BAD_PASSPHRASE", "GOOD_PASSPHRASE", - "MISSING_PASSPHRASE", "DECRYPTION_FAILED", - "KEY_NOT_CREATED"): - self.status = key.replace("_", " ").lower() - elif key == "NEED_PASSPHRASE_SYM": - self.status = 'need symmetric passphrase' - elif key == "BEGIN_DECRYPTION": - self.status = 'decryption incomplete' - elif key == "BEGIN_ENCRYPTION": - self.status = 'encryption incomplete' - elif key == "DECRYPTION_OKAY": - self.status = 'decryption ok' - self.ok = True - elif key == "END_ENCRYPTION": - self.status = 'encryption ok' - self.ok = True - elif key == "INV_RECP": - self.status = 'invalid recipient' - elif key == "KEYEXPIRED": - self.status = 'key expired' - elif key == "SIG_CREATED": - self.status = 'sig created' - elif key == "SIGEXPIRED": - self.status = 'sig expired' - else: - Verify._handle_status(self, key, value) - class GenKey(object): - """Handle status messages for --gen-key""" + """Handle status messages for key generation. + + Calling the GenKey.__str__() method of this class will return the + generated key's fingerprint, or a status string explaining the results. + """ def __init__(self, gpg): self.gpg = gpg + ## this should get changed to something more useful, like 'key_type' + #: 'P':= primary, 'S':= subkey, 'B':= both self.type = None self.fingerprint = None From 18ffc52aca5abf352b141f56b9f76e6ac99c8e27 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:33:57 +0000 Subject: [PATCH 331/397] Add better parsing of PROGRESS and NODATA status codes in _parsers.GenKey. --- src/_parsers.py | 55 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index d32a87c..0f85829 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -621,6 +621,33 @@ def _sanitise_list(arg_list): yield safe_arg +def nodata(status_code): + """Translate NODATA status codes from GnuPG to messages.""" + lookup = { + '1': 'No armored data.', + '2': 'Expected a packet but did not find one.', + '3': 'Invalid packet found, this may indicate a non OpenPGP message.', + '4': 'Signature expected but not found.' } + for key, value in lookup.items(): + if str(status_code) == key: + return value + +def progress(status_code): + """Translate PROGRESS status codes from GnuPG to messages.""" + lookup = { + 'pk_dsa': 'DSA key generation', + 'pk_elg': 'Elgamal key generation', + 'primegen': 'Prime generation', + 'need_entropy': 'Waiting for new entropy in the RNG', + 'tick': 'Generic tick without any special meaning - still working.', + 'starting_agent': 'A gpg-agent was started.', + 'learncard': 'gpg-agent or gpgsm is learning the smartcard data.', + 'card_busy': 'A smartcard is still working.' } + for key, value in lookup.items(): + if str(status_code) == key: + return value + + class GenKey(object): """Handle status messages for key generation. @@ -633,28 +660,48 @@ class GenKey(object): #: 'P':= primary, 'S':= subkey, 'B':= both self.type = None self.fingerprint = None + self.status = None + self.subkey_created = False + self.primary_created = False def __nonzero__(self): if self.fingerprint: return True return False - __bool__ = __nonzero__ def __str__(self): - return self.fingerprint or '' + if self.fingerprint: + return self.fingerprint + else: + if self.status is not None: + return self.status + else: + return False - def _handle_status(self, key, value): + def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. """ - if key in ("PROGRESS", "GOOD_PASSPHRASE", "NODATA", "KEY_NOT_CREATED"): + if key in ("GOOD_PASSPHRASE"): pass + elif key == "KEY_NOT_CREATED": + self.status = 'key not created' elif key == "KEY_CREATED": (self.type, self.fingerprint) = value.split() + self.status = 'key created' + elif key == "NODATA": + self.status = nodata(value) + elif key == "PROGRESS": + self.status = progress(value.split(' ', 1)[0]) else: raise ValueError("Unknown status message: %r" % key) + if self.type in ('B', 'P'): + self.primary_key_created = True + if self.type in ('B', 'S'): + self.subkey_created = True + class DeleteResult(object): """Handle status messages for --delete-keys and --delete-secret-keys""" def __init__(self, gpg): From 75f3a4a89e697ebe7710a7e79f3f16b574268a7f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:36:28 +0000 Subject: [PATCH 332/397] =?UTF-8?q?=5Fhandle=5Fstatus=E2=86=92handle=5Fsta?= =?UTF-8?q?tus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/_parsers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index 0f85829..f6a9b43 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -715,7 +715,7 @@ class DeleteResult(object): '2': 'Must delete secret key first', '3': 'Ambigious specification', } - def _handle_status(self, key, value): + def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. @@ -760,7 +760,7 @@ class Sign(object): def __str__(self): return self.data.decode(self.gpg.encoding, self.gpg._decode_errors) - def _handle_status(self, key, value): + def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. @@ -830,7 +830,7 @@ class ListKeys(list): subkey = [args[4], args[11]] self.curkey['subkeys'].append(subkey) - def _handle_status(self, key, value): + def handle_status(self, key, value): pass @@ -885,7 +885,7 @@ class ImportResult(object): '3': 'Certificate Chain too long', '4': 'Error storing certificate', } - def _handle_status(self, key, value): + def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. @@ -986,7 +986,7 @@ class Verify(object): return self.valid __bool__ = __nonzero__ - def _handle_status(self, key, value): + def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. @@ -1059,7 +1059,7 @@ class ListPackets(object): self.need_passphrase_sym = None self.userid_hint = None - def _handle_status(self, key, value): + def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. :raises: :exc:`ValueError` if the status message is unknown. From 596d6cd2d3671ca661f7c243ba1ab7911083a549 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:37:05 +0000 Subject: [PATCH 333/397] Parse NODATA status code in _parsers.Sign. --- src/_parsers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/_parsers.py b/src/_parsers.py index f6a9b43..830e1b8 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -767,11 +767,13 @@ class Sign(object): """ if key in ("USERID_HINT", "NEED_PASSPHRASE", "BAD_PASSPHRASE", "GOOD_PASSPHRASE", "BEGIN_SIGNING", "CARDCTRL", - "INV_SGNR", "NODATA"): + "INV_SGNR"): pass elif key == "SIG_CREATED": (self.sig_type, self.sig_algo, self.sig_hash_algo, self.what, self.timestamp, self.fingerprint) = value.split() + elif key == "NODATA": + self.status = nodata(value) else: raise ValueError("Unknown status message: %r" % key) From bac7e52fc0c9dcabfb80d59ef50dc81421986ff9 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:37:37 +0000 Subject: [PATCH 334/397] Update docs for _parsers.Verify. --- src/_parsers.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index 830e1b8..2b7e165 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -938,21 +938,13 @@ class ImportResult(object): class Verify(object): - """Classes for parsing GnuPG status messages for signature verification. + """Parser for internal status messages from GnuPG for + certification/signature verification, and for parsing portions of status + messages from decryption operations. :type gpg: :class:`gnupg.GPG` :param gpg: An instance of :class:`gnupg.GPG`. - :attr bool valid: True if the signature or file was verified successfully, - False otherwise. - :attr str fingerprint: The fingerprint of the GnuPG keyID which created the - signature. - - :attr str creation_date: The date the signature was made. - :attr str timestamp: The timestamp used internally in the signature. - :attr str signature_id: The uid of the signing GnuPG key. - :attr str status: The internal status message from the GnuPG process. """ - ## xxx finish documentation TRUST_UNDEFINED = 0 TRUST_NEVER = 1 From 9aa7ad8c696f38354ed579109c1b0bac4d5628a7 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:38:35 +0000 Subject: [PATCH 335/397] Add class attrs and docs to Verify, add new _parsers.Crypt class. --- src/_parsers.py | 98 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/src/_parsers.py b/src/_parsers.py index 2b7e165..5306837 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -958,6 +958,37 @@ class Verify(object): "TRUST_FULLY" : TRUST_FULLY, "TRUST_ULTIMATE" : TRUST_ULTIMATE,} + #: True if the signature is valid, False otherwise. + valid = False + #: A string describing the status of the signature verification. + #: Can be one of ``'signature bad'``, ``'signature good'``, + #: ``'signature valid'``, ``'signature error'``, ``'decryption failed'``, + #: ``'no public key'``, ``'key exp'``, or ``'key rev'``. + status = None + #: The fingerprint of the signing keyid. + fingerprint = None + #: The fingerprint of the corresponding public key, which may be different + #: if the signature was created with a subkey. + pubkey_fingerprint = None + #: The keyid of the signing key. + key_id = None + #: The id of the signature itself. + signature_id = None + #: The creation date of the signing key. + creation_date = None + #: The timestamp of the purported signature, if we are unable to parse it. + timestamp = None + #: The userid of the signing key which was used to create the signature. + username = None + #: When the signing key is due to expire. + expire_timestamp = None + #: The timestamp for when the signature was created. + sig_timestamp = None + #: A number 0-4 describing the trust level of the signature. + trust_level = None + #: The string corresponding to the ``trust_level`` number. + trust_text = None + def __init__(self, gpg): self.gpg = gpg self.valid = False @@ -1040,6 +1071,73 @@ class Verify(object): else: raise ValueError("Unknown status message: %r" % key) + +class Crypt(Verify): + """Parser for internal status messages from GnuPG for ``--encrypt``, + ``--decrypt``, and ``--decrypt-files``. + """ + def __init__(self, gpg): + Verify.__init__(self, gpg) + self.gpg = gpg + self.data = '' + self.ok = False + self.status = '' + self.data_format = None + self.data_timestamp = None + self.data_filename = None + + def __nonzero__(self): + if self.ok: return True + return False + __bool__ = __nonzero__ + + def __str__(self): + return self.data.decode(self.gpg.encoding, self.gpg._decode_errors) + + def handle_status(self, key, value): + """Parse a status code from the attached GnuPG process. + + :raises: :exc:`ValueError` if the status message is unknown. + """ + if key in ("ENC_TO", "USERID_HINT", "GOODMDC", "END_DECRYPTION", + "BEGIN_SIGNING", "NO_SECKEY", "ERROR", "NODATA", + "CARDCTRL"): + # in the case of ERROR, this is because a more specific error + # message will have come first + pass + elif key in ("NEED_PASSPHRASE", "BAD_PASSPHRASE", "GOOD_PASSPHRASE", + "MISSING_PASSPHRASE", "DECRYPTION_FAILED", + "KEY_NOT_CREATED"): + self.status = key.replace("_", " ").lower() + elif key == "NEED_PASSPHRASE_SYM": + self.status = 'need symmetric passphrase' + elif key == "BEGIN_DECRYPTION": + self.status = 'decryption incomplete' + elif key == "BEGIN_ENCRYPTION": + self.status = 'encryption incomplete' + elif key == "DECRYPTION_OKAY": + self.status = 'decryption ok' + self.ok = True + elif key == "END_ENCRYPTION": + self.status = 'encryption ok' + self.ok = True + elif key == "INV_RECP": + self.status = 'invalid recipient' + elif key == "KEYEXPIRED": + self.status = 'key expired' + elif key == "SIG_CREATED": + self.status = 'sig created' + elif key == "SIGEXPIRED": + self.status = 'sig expired' + elif key == "PLAINTEXT": + fmt, self.data_timestamp, self.data_filename = value.split(' ', 2) + ## GnuPG give us a hex byte for an ascii char corresponding to + ## the data format of the resulting plaintext, + ## i.e. '62'→'b':= binary data + self.data_format = chr(int(str(fmt), 16)) + else: + Verify.handle_status(key, value) + class ListPackets(object): """ Handle status messages for --list-packets. From b50e91bd5e89662f797272900da657e8d259816d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 22 May 2013 17:58:22 +0000 Subject: [PATCH 336/397] Remove two extra functions from GPGWrapper class. --- src/gnupg.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index e11e1af..1a8aebc 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1349,7 +1349,7 @@ use_agent: %s :param str output: The output file to write to. If not specified, the encrypted output is returned, and thus should be stored as an object in Python. For example: - + """ args = list() ## both can be used at the same time for an encrypted file which @@ -1479,17 +1479,6 @@ class GPGWrapper(GPG): This is a temporary class for handling GPG requests, and should be replaced by a more general class used throughout the project. """ - - def __init__(self, binary=None, homedir=_conf, - verbose=False, use_agent=False, keyring=None, options=None): - super(GPGWrapper, self).__init__(gnupghome=gnupghome, - binary=binary, - verbose=verbose, - use_agent=use_agent, - keyring=keyring, - options=options) - self._result_map['list-packets'] = ListPackets - def find_key_by_email(self, email, secret=False): """ Find user's key based on their email. @@ -1508,13 +1497,6 @@ class GPGWrapper(GPG): raise LookupError( "GnuPG public key for subkey %s not found!" % subkey) - def find_key_by_keyid(self, keyid): - for key in self.list_keys(): - if keyid == key['keyid']: - return key - raise LookupError( - "GnuPG public key for subkey %s not found!" % subkey) - def encrypt(self, data, recipient, sign_with=None, always_trust=True, passphrase=None, symmetric=False): """ From 55ca5487bbc9f580f21be2e4cee3e14bcd25afbf Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 23 May 2013 05:15:22 +0000 Subject: [PATCH 337/397] Change sign/sign_with param in GPG.encrypt* to be named 'default_key'. --- src/gnupg.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 1a8aebc..5cd1e61 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1377,9 +1377,9 @@ use_agent: %s if armor: args.append('--armor') - if sign_with: + if default_key: args.append('--sign') - args.append('--default-key %s' % sign_with) + args.append('--default-key %s' % default_key) if digest_algo: args.append('--digest-algo %s' % digest_algo) if always_trust: @@ -1497,14 +1497,14 @@ class GPGWrapper(GPG): raise LookupError( "GnuPG public key for subkey %s not found!" % subkey) - def encrypt(self, data, recipient, sign_with=None, always_trust=True, + def encrypt(self, data, recipient, default_key=None, always_trust=True, passphrase=None, symmetric=False): """ Encrypt data using GPG. """ # TODO: devise a way so we don't need to "always trust". return super(GPGWrapper, self).encrypt(data, recipient, - sign_with=sign, + default_key=default_key, always_trust=always_trust, passphrase=passphrase, symmetric=symmetric, From de9eb4ca77fc8bc410bd9cfda5f9b2104c3dd575 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 23 May 2013 07:03:55 +0000 Subject: [PATCH 338/397] Add utility for getting the timestamp in ISO 8601 format. --- src/_util.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/_util.py b/src/_util.py index f5c0c85..bc43826 100644 --- a/src/_util.py +++ b/src/_util.py @@ -318,6 +318,10 @@ def _next_year(): next_year = str(int(year)+1) return '-'.join((next_year, month, day)) +def _now(): + """Get a timestamp for right now, formatted according to ISO 8601.""" + return datetime.isoformat(datetime.now()) + def _threaded_copy_data(instream, outstream): """Copy data from one stream to another in a separate thread. From c7a4845ce4fd937fb86334c76d7f175f305740ff Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 23 May 2013 07:05:14 +0000 Subject: [PATCH 339/397] Also get set GPG.filesystemencoding in case we have to save a file. --- src/gnupg.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index 5cd1e61..2795087 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -89,6 +89,7 @@ import codecs ## For AOS, the locale module will need to point to a wrapper around the ## java.util.Locale class. ## See https://code.patternsinthevoid.net/?p=android-locale-hack.git +import encodings import locale import logging import os @@ -189,7 +190,8 @@ class GPGBase(object): if encoding is None: # This happens on Jython! encoding = sys.stdin.encoding self.encoding = encoding.lower().replace('-', '_') - + self.filesystemencoding = encodings.normalize_encoding( + sys.getfilesystemencoding().lower()) try: assert self.binary, "Could not find binary %s" % binary From 8afd189d3622aadc561e5dc0e4118f66024fe1f2 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 23 May 2013 07:06:11 +0000 Subject: [PATCH 340/397] =?UTF-8?q?InheritablePropery=E2=86=92=5Futil.Inhe?= =?UTF-8?q?ritableProperty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gnupg.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 2795087..d34179a 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -101,7 +101,6 @@ import threading from _parsers import _fix_unsafe, _sanitise, _is_allowed, _sanitise_list from _parsers import _check_preferences -from _util import InheritableProperty from _util import _conf, _is_list_or_tuple, _is_stream from _util import _make_binary_stream from _util import log @@ -378,7 +377,7 @@ class GPGBase(object): log.info("Setting homedir to '%s'" % hd) self._homedir = hd - homedir = InheritableProperty(_homedir_getter, _homedir_setter) + homedir = _util.InheritableProperty(_homedir_getter, _homedir_setter) class GPG(GPGBase): From 51e2b332651cfd1349d803703c8275cf3028552b Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 23 May 2013 08:47:47 +0000 Subject: [PATCH 341/397] Add .../homedir/batch-files and .../homedir/generated-keys directories. --- src/gnupg.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/gnupg.py b/src/gnupg.py index d34179a..93e1c0f 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -475,6 +475,9 @@ use_agent: %s self.default_preference_list, self.options, str(self.verbose), str(self.use_agent))) + self._batch_dir = os.path.join(self.homedir, 'batch-files') + self._keys_dir = os.path.join(self.homedir, 'generated-keys') + ## check that everything runs alright: proc = self._open_subprocess(["--list-config", "--with-colons"]) result = self._result_map['list'](self) From 22aa8d2ee120d9e462e0e1b8d4d81b34f0f3b807 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 23 May 2013 08:49:27 +0000 Subject: [PATCH 342/397] Add a parameter to GPG.gen_key_input for saving batch files. --- src/gnupg.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index 93e1c0f..da1a60a 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1082,7 +1082,7 @@ use_agent: %s f.close() return key - def gen_key_input(self, testing=False, **kwargs): + def gen_key_input(self, save=False, testing=False, **kwargs): """Generate a batch file for input to :meth:`GPG.gen_key()`. The GnuPG batch file key generation feature allows unattended key @@ -1151,6 +1151,11 @@ use_agent: %s destroyed, and never for the generation of actual use keys. + :param bool save: Save a copy of the generated batch file to disk in a + file named .batch, where is + the ``name_real`` parameter stripped of punctuation, + spaces, and non-ascii characters. + :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_email: The email in the UID of the generated key. From 9e5bdafffa73c0bb365217a6db064170315eaeb6 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 23 May 2013 08:52:18 +0000 Subject: [PATCH 343/397] Remove excess EOL whitespace. --- src/gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index da1a60a..bc527e4 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1301,7 +1301,7 @@ use_agent: %s ## to set 'cert', 'Key-Usage' must be blank string if not key in ('Key-Usage', 'Subkey-Usage'): ## otherwise skip anything else that's empty - if str(val).strip(): + if str(val).strip(): parms[key] = val ## Key-Type must come first, followed by length From 0921ca7a2b9a82d058010652846cc74fa9f56b71 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 23 May 2013 08:54:05 +0000 Subject: [PATCH 344/397] If Key-Type is 'default', force 'Subkey-Type' to also be 'default'. --- src/gnupg.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index bc527e4..fb42442 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1304,13 +1304,23 @@ use_agent: %s if str(val).strip(): parms[key] = val + ## if Key-Type is 'default', make Subkey-Type also be 'default' + if parms['Key-Type'] == 'default': + subkey_must_be_default = True + for field in ('Key-Usage', 'Subkey-Usage',): + try: parms.pop(field) ## usage shouldn't be specified + except KeyError: pass + ## Key-Type must come first, followed by length out = "Key-Type: %s\n" % parms.pop('Key-Type') out += "Key-Length: %d\n" % parms.pop('Key-Length') if 'Subkey-Type' in parms.keys(): out += "Subkey-Type: %s\n" % parms.pop('Subkey-Type') - if 'Subkey-Length' in parms.keys(): - out += "Subkey-Length: %s\n" % parms.pop('Subkey-Length') + else: + if subkey_must_be_default: + out += "Subkey-Type: default\n" + if 'Subkey-Length' in parms.keys(): + out += "Subkey-Length: %s\n" % parms.pop('Subkey-Length') if 'Name-Real' in parms.items(): ## xxx für die Dateinamen From 4c67e4575927fafacc9c9eb9666bf8708496554b Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Thu, 23 May 2013 08:56:20 +0000 Subject: [PATCH 345/397] Add batch file save functionality, including creating a README in that dir. --- src/gnupg.py | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index fb42442..0f8f4cf 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1273,8 +1273,8 @@ use_agent: %s :returns: A suitable input string for the ``GPG.gen_key()`` method, the latter of which will create the new keypair. - see http://www.gnupg.org/documentation/manuals/gnupg-devel/Unatten\ - ded-GPG-key-generation.html#Unattended-GPG-key-generation + see + http://www.gnupg.org/documentation/manuals/gnupg-devel/Unattended-GPG-key-generation.html for more details. """ @@ -1288,8 +1288,8 @@ use_agent: %s try: logname = os.environ['LOGNAME'] except KeyError: logname = os.environ['USERNAME'] hostname = socket.gethostname() - parms.setdefault('Name-Email', "%s@%s" - % (logname.replace(' ', '_'), hostname)) + uidname = "%s@%s" % (logname.replace(' ', '_'), hostname) + parms.setdefault('Name-Email', uidname) if testing: ## This specific comment string is required by (some? all?) @@ -1322,10 +1322,6 @@ use_agent: %s if 'Subkey-Length' in parms.keys(): out += "Subkey-Length: %s\n" % parms.pop('Subkey-Length') - if 'Name-Real' in parms.items(): - ## xxx für die Dateinamen - name = real_name.lower().replace(' ', '') - for key, val in list(parms.items()): out += "%s: %s\n" % (key, val) @@ -1340,6 +1336,38 @@ use_agent: %s out += "%transient-key\n" out += "%commit\n" + + ## if we've been asked to save a copy of the batch file: + if save and parms['Name-Email'] != uidname: + asc_uid = encodings.normalize_encoding(parms['Name-Email']) + filename = _fix_unsafe(asc_uid) + _util._now() + '.batch' + save_as = os.path.join(self._batch_dir, filename) + readme = os.path.join(self._batch_dir, 'README') + + if not os.path.exists(self._batch_dir): + os.makedirs(self._batch_dir) + + ## the following pulls the link to GnuPG's online batchfile + ## documentation from this function's docstring and sticks it + ## in a README file in the batch directory: + + if getattr(self.gen_key_input, '__doc__', None) is not None: + docs = self.gen_key_input.__doc__ + else: + docs = str() ## no docstrings if run with "python -OO" + links = '\n'.join(x.strip() for x in docs.splitlines()[-2:]) + explain = """ +This directory was created by python-gnupg, on {}, and +it contains saved batch files, which can be given to GnuPG to automatically +generate keys. Please see +{}""".format(_util.now(), links) ## sometimes python is awesome. + + with open(readme, 'a+') as fh: + [fh.write(line) for line in explain] + + with open(save_as, 'a+') as batch_file: + [batch_file.write(line) for line in out] + return out def encrypt_file(self, filename, recipients, default_key=None, From 7700b012b12fddad1395243212af13c095dc3c82 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 07:12:50 +0000 Subject: [PATCH 346/397] Only gnupg.__version__, not gnupg._version, should be accessible in module. --- src/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/__init__.py b/src/__init__.py index 5cd8a1a..43a5b8e 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -18,3 +18,4 @@ __all__ = ["GPG"] del gnupg del copyleft del get_versions +del _version From 30223d42032caabeb63c714bbf276817c6158d3b Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 07:14:03 +0000 Subject: [PATCH 347/397] Don't attempt to set up logging to file unless logging is actually enabled. --- src/_logger.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_logger.py b/src/_logger.py index 6fe6a66..536b485 100644 --- a/src/_logger.py +++ b/src/_logger.py @@ -66,13 +66,14 @@ def create_logger(level=logging.NOTSET): _fn = os.path.join(_test, "%s_test_gnupg.log" % _now) _fmt = "%(relativeCreated)-4d L%(lineno)-4d:%(funcName)-18.18s %(levelname)-7.7s %(message)s" - logging.basicConfig(level=level, filename=_fn, filemode="a", format=_fmt) ## Add the GNUPG_STATUS_LEVEL LogRecord to all Loggers in the module: logging.addLevelName(GNUPG_STATUS_LEVEL, "GNUPG") logging.Logger.status = status if level > logging.NOTSET: + logging.basicConfig(level=level, filename=_fn, + filemode="a", format=_fmt) logging.captureWarnings(True) logging.logThreads = True From 69c6981b6637b38914d7f1d9da64253ee23fe910 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 07:15:33 +0000 Subject: [PATCH 348/397] A unicode(list()) versus a unicode(str()) are different. --- src/_parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_parsers.py b/src/_parsers.py index 5306837..8b1ebba 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -475,7 +475,7 @@ def _sanitise(*args): safe_option = str() if not _util._py3k: - if isinstance(arg, unicode): + if not isinstance(arg, list) and isinstance(arg, unicode): arg = str(arg) try: From f65022500d4d56045633dbe3121d252010abe4e3 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 07:19:40 +0000 Subject: [PATCH 349/397] Add note on using itertools.dropwhile() for efficiency in _is_allowed(). * If/when it's rewritten, a simple speed/efficiency check can be done with: >>> import dis >>> dis.dis(_is_allowed_orig('--dragons')) >>> dis.dis(_is_allowed_orig('--encrypt')) >>> dis.dis(_is_allowed_new('--dragons')) >>> dis.dis(_is_allowed_new('--encrypt')) --- src/_parsers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_parsers.py b/src/_parsers.py index 8b1ebba..83c4472 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -404,6 +404,7 @@ def _is_allowed(input): hyphenated = _hyphenate(input) else: hyphenated = input + ## xxx we probably want to use itertools.dropwhile here try: assert hyphenated in allowed except AssertionError as ae: From e52e9001d1673bb2bbfe95c920900a6c1ab7abcc Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 07:24:31 +0000 Subject: [PATCH 350/397] Rewrite _check_option() in _sanitise() to be way more efficient and readable. --- src/_parsers.py | 86 +++++++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index 83c4472..db7a77b 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -490,46 +490,54 @@ def _sanitise(*args): or (_util._py3k and isinstance(value, str)): values = value.split(' ') for v in values: + try: + assert v is not None + assert v.strip() != "" + except: + log.debug("Dropping %s %s" % (flag, v)) + continue + + ## these can be handled separately, without _fix_unsafe(), + ## because they are only allowed if the pass the regex + if flag in ['--default-key', '--recipient', '--export', + '--export-secret-keys', '--delete-keys', + '--list-sigs', '--export-secret-subkeys',]: + if _is_hex(v): + safe_option += (v + " ") + continue + else: log.debug("'%s %s' not hex." % (flag, v)) + val = _fix_unsafe(v) - if val is not None and val.strip() != "": - if flag in ['--encrypt', '--encrypt-files', '--decrypt', - '--decrypt-files', '--import', '--verify']: - ## Place checks here: - if _util._is_file(val): - safe_option += (val + " ") - else: - log.debug("%s not file: %s" % (flag, val)) - elif flag in ['--default-key', '--recipient', - '--export', '--export-secret-keys', - '--delete-keys', '--list-sigs', - '--export-secret-subkeys',]: - if _is_hex(val): - safe_option += (val + " ") - else: - log.debug("'%s %s' not hex." % (flag, val)) - elif flag in ['--cipher-algo', - '--personal-cipher-prefs', - '--personal-cipher-preferences']: - legit_algos = _check_preferences(val, 'cipher') - if legit_algos: - safe_option += (legit_algos + " ") - else: - log.debug("'%s' is not cipher" - % _fix_unsafe(val)) - elif flag in ['--compress-algo', - '--compression-algo', - '--personal-compress-prefs', - '--personal-compress-preferences']: - legit_algos = _check_preferences(val, 'compress') - if legit_algos: - safe_option += (legit_algos + " ") - else: - log.debug("'%s' not compress algo" - % _fix_unsafe(val)) - else: - safe_option += (val + " ") - log.debug("_check_option(): No checks for %s" - % val) + + try: + assert v is not None + assert v.strip() != "" + except: + log.debug("Dropping %s %s" % (flag, v)) + continue + + if flag in ['--encrypt', '--encrypt-files', '--decrypt', + '--decrypt-files', '--import', '--verify']: + if _util._is_file(val): safe_option += (val + " ") + else: log.debug("%s not file: %s" % (flag, val)) + + elif flag in ['--cipher-algo', '--personal-cipher-prefs', + '--personal-cipher-preferences']: + legit_algos = _check_preferences(val, 'cipher') + if legit_algos: safe_option += (legit_algos + " ") + else: log.debug("'%s' is not cipher" % val) + + elif flag in ['--compress-algo', '--compression-algo', + '--personal-compress-prefs', + '--personal-compress-preferences']: + legit_algos = _check_preferences(val, 'compress') + if legit_algos: safe_option += (legit_algos + " ") + else: log.debug("'%s' not compress algo" % val) + + else: + safe_option += (val + " ") + log.debug("_check_option(): No checks for %s" % val) + return safe_option is_flag = lambda x: x.startswith('--') From c3dcd34f5a2106cef08b57ccb57d20d00528a231 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 07:25:27 +0000 Subject: [PATCH 351/397] This log statement didn't need 2LOC. --- src/_parsers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index db7a77b..1ae5046 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -562,8 +562,7 @@ def _sanitise(*args): else: groups[last] = str() while len(filo) > 1 and not is_flag(filo[len(filo)-1]): - log.debug("Got value: %s" - % filo[len(filo)-1]) + log.debug("Got value: %s" % filo[len(filo)-1]) groups[last] += (filo.pop() + " ") else: if len(filo) == 1 and not is_flag(filo[0]): From c31e1303ba622ed17687e26f8c9ce7dc3f2245c3 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 07:26:15 +0000 Subject: [PATCH 352/397] GnuPG status code '17' on imports means '16'. That wasn't documented anywhere. --- src/_parsers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_parsers.py b/src/_parsers.py index 1ae5046..b493352 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -887,7 +887,8 @@ class ImportResult(object): '2': 'New user IDs', '4': 'New signatures', '8': 'New subkeys', - '16': 'Contains private key',} + '16': 'Contains private key', + '17': 'Contains private key',} problem_reason = { '0': 'No specific reason given', '1': 'Invalid Certificate', From 1c3f0c3c9a674eb4db4536e5b47cc13afecd0344 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 07:28:03 +0000 Subject: [PATCH 353/397] There are lots of these list(dict.items()) in the old codebase. Total nonsense. * These evaluate in all interpreters to functionally the same bytecode, with an extra LOAD_GLOBAL and then a CALL_FUNCTION for that GLOBAL which doesn't gets a passthrough value. I have absolutely no idea why someone thought it was necessary to code it this way, unless there used to be some weird dict bug in some version of Python (which is totally possible) that I don't remember and have never heard about. Disassembled: >>> def normal(things): ... for k,v in things.items(): ... print "%s=%s" % (k,v) ... ... >>> def notnormal(things): ... for k,v in list(things.items()): ... print "%s=%s" % (k,v) ... >>> dis.dis(normal) 2 0 SETUP_LOOP 41 (to 44) 3 LOAD_FAST 0 (things) 6 LOAD_ATTR 0 (items) 9 CALL_FUNCTION 0 12 GET_ITER >> 13 FOR_ITER 27 (to 43) 16 UNPACK_SEQUENCE 2 19 STORE_FAST 1 (k) 22 STORE_FAST 2 (v) 3 25 LOAD_CONST 1 ('%s=%s') 28 LOAD_FAST 1 (k) 31 LOAD_FAST 2 (v) 34 BUILD_TUPLE 2 37 BINARY_MODULO 38 PRINT_ITEM 39 PRINT_NEWLINE 40 JUMP_ABSOLUTE 13 >> 43 POP_BLOCK >> 44 LOAD_CONST 0 (None) 47 RETURN_VALUE >>> dis.dis(notnormal) 2 0 SETUP_LOOP 47 (to 50) 3 LOAD_GLOBAL 0 (list) 6 LOAD_FAST 0 (things) 9 LOAD_ATTR 1 (items) 12 CALL_FUNCTION 0 15 CALL_FUNCTION 1 18 GET_ITER >> 19 FOR_ITER 27 (to 49) 22 UNPACK_SEQUENCE 2 25 STORE_FAST 1 (k) 28 STORE_FAST 2 (v) 3 31 LOAD_CONST 1 ('%s=%s') 34 LOAD_FAST 1 (k) 37 LOAD_FAST 2 (v) 40 BUILD_TUPLE 2 43 BINARY_MODULO 44 PRINT_ITEM 45 PRINT_NEWLINE 46 JUMP_ABSOLUTE 19 >> 49 POP_BLOCK >> 50 LOAD_CONST 0 (None) 53 RETURN_VALUE --- src/_parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_parsers.py b/src/_parsers.py index b493352..df557fb 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -910,7 +910,7 @@ class ImportResult(object): elif key == "IMPORT_OK": reason, fingerprint = value.split() reasons = [] - for code, text in list(self.ok_reason.items()): + for code, text in self.ok_reason.items(): if int(reason) | int(code) == int(reason): reasons.append(text) reasontext = '\n'.join(reasons) + "\n" From 1d326f880813a39d977afdc811de04b775b20c1c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 07:46:54 +0000 Subject: [PATCH 354/397] GnuPG only *sometimes* returns the filename for the plaintext. * We can't split twice at first, because sometimes there isn't a third field in the string. * There isn't any rhyme or reason to when the filename is present or missing. It just is. --- src/_parsers.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/_parsers.py b/src/_parsers.py index df557fb..44dad8d 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -1139,10 +1139,14 @@ class Crypt(Verify): elif key == "SIGEXPIRED": self.status = 'sig expired' elif key == "PLAINTEXT": - fmt, self.data_timestamp, self.data_filename = value.split(' ', 2) - ## GnuPG give us a hex byte for an ascii char corresponding to - ## the data format of the resulting plaintext, - ## i.e. '62'→'b':= binary data + fmt, dts = value.split(' ', 1) + if dts.find(' ') > 0: + self.data_timestamp, self.data_filename = dts.split(' ', 1) + else: + self.data_timestamp = dts + ## GnuPG give us a hex byte for an ascii char corresponding to + ## the data format of the resulting plaintext, + ## i.e. '62'→'b':= binary data self.data_format = chr(int(str(fmt), 16)) else: Verify.handle_status(key, value) From e252f633411e28905d9faaa8176c7bd4a7d4dc62 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 07:49:10 +0000 Subject: [PATCH 355/397] Switch to calling a bound super on Crypt for handle_status() method again. --- src/_parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_parsers.py b/src/_parsers.py index 44dad8d..3d5c1d5 100644 --- a/src/_parsers.py +++ b/src/_parsers.py @@ -1149,7 +1149,7 @@ class Crypt(Verify): ## i.e. '62'→'b':= binary data self.data_format = chr(int(str(fmt), 16)) else: - Verify.handle_status(key, value) + super(Crypt, self).handle_status(key, value) class ListPackets(object): """ From cce8785f392cb06d43d9c5989fecd420f603f16e Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 07:50:36 +0000 Subject: [PATCH 356/397] Slice equiv. bytes from strings as we read() from file handles in _copy_data(). --- src/_util.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/_util.py b/src/_util.py index bc43826..bd1a027 100644 --- a/src/_util.py +++ b/src/_util.py @@ -119,7 +119,12 @@ def _copy_data(instream, outstream): coder = find_encodings() while True: - data = instream.read(1024) + if ((_py3k and isinstance(instream, str)) or + (not _py3k and isinstance(instream, basestring))): + data = instream[:1024] + instream = instream[1024:] + else: + data = instream.read(1024) if len(data) == 0: break sent += len(data) From bf591c2dd0dee6d84f2d5223ecbe84bc9f89715a Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 08:12:54 +0000 Subject: [PATCH 357/397] Add a utility for creating a 'user@hostname' string. --- src/_util.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/_util.py b/src/_util.py index bd1a027..1ebc8f3 100644 --- a/src/_util.py +++ b/src/_util.py @@ -22,6 +22,7 @@ Extra utilities for python-gnupg. ''' from datetime import datetime +from socket import gethostname import codecs import encodings @@ -173,6 +174,39 @@ def _create_if_necessary(directory): log.debug("Created directory.") return True +def create_uid_email(username=None, hostname=None): + """Create an email address suitable for a UID on a GnuPG key. + + :param str username: The username portion of an email address. If None, + defaults to the username of the running Python + process. + + :param str hostname: The FQDN portion of an email address. If None, the + hostname is obtained from gethostname(2). + + :rtype: str + :returns: A string formatted as @. + """ + if hostname: + hostname = hostname.replace(' ', '_') + if not username: + try: username = os.environ['LOGNAME'] + except KeyError: username = os.environ['USERNAME'] + + if not hostname: hostname = gethostname() + + uid = "%s@%s" % (username.replace(' ', '_'), hostname) + else: + username = username.replace(' ', '_') + if (not hostname) and (username.find('@') == 0): + uid = "%s@%s" % (username, gethostname()) + elif hostname: + uid = "%s@%s" % (username, hostname) + else: + uid = username + + return uid + def _find_binary(binary=None): """Find the absolute path to the GnuPG binary. From 50b057918b66ed59b0742318b88da82131224b2c Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 08:13:27 +0000 Subject: [PATCH 358/397] Change line to check multiple types in the same isinstance() call. --- src/_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_util.py b/src/_util.py index 1ebc8f3..34a6e45 100644 --- a/src/_util.py +++ b/src/_util.py @@ -282,7 +282,7 @@ def _is_list_or_tuple(instance): :rtype: bool :returns: True if ``instance`` is a list or tuple, False otherwise. """ - return isinstance(instance,list) or isinstance(instance,tuple) + return isinstance(instance, (list, tuple,)) def _make_binary_stream(s, encoding): """ From a0e1d4db6ea69844f36ab55a387cfca044d5e8c1 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 08:14:08 +0000 Subject: [PATCH 359/397] Add a utility for getting the current UTC seconds-since-epoch time. --- src/_util.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/_util.py b/src/_util.py index 34a6e45..bbfe3c9 100644 --- a/src/_util.py +++ b/src/_util.py @@ -377,6 +377,10 @@ def _threaded_copy_data(instream, outstream): copy_thread.start() return copy_thread +def _utc_epoch(): + """Get the seconds since epoch for UTC.""" + return int(time.mktime(time.gmtime())) + def _which(executable, flags=os.X_OK): """Borrowed from Twisted's :mod:twisted.python.proutils . From efc10b6791772872a5ac4ace10f7eeb069eae838 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 08:14:52 +0000 Subject: [PATCH 360/397] Remove unused import for socket module. --- src/gnupg.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index 0f8f4cf..ae44d36 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -94,7 +94,6 @@ import locale import logging import os import re -import socket import sys import tempfile import threading From 1dfadbf32fdf4a14a13e226fd193eef1c605f125 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 08:15:10 +0000 Subject: [PATCH 361/397] Remove excessive log statement. --- src/gnupg.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index ae44d36..d7c99b5 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -372,7 +372,6 @@ class GPGBase(object): log.debug(ae.message) raise RuntimeError(ae.message) else: - log.debug("GPGBase:") log.info("Setting homedir to '%s'" % hd) self._homedir = hd From e501afb6a77da414492b8871cccd885c2ef8eb13 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 08:15:50 +0000 Subject: [PATCH 362/397] Add initialization of GPG.temp_keyring and GPG.temp_secring objects. --- src/gnupg.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index d7c99b5..642bcfc 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -474,7 +474,12 @@ use_agent: %s str(self.use_agent))) self._batch_dir = os.path.join(self.homedir, 'batch-files') - self._keys_dir = os.path.join(self.homedir, 'generated-keys') + self._key_dir = os.path.join(self.homedir, 'generated-keys') + + #: The keyring used in the most recently created batch file + self.temp_keyring = None + #: The secring used in the most recently created batch file + self.temp_secring = None ## check that everything runs alright: proc = self._open_subprocess(["--list-config", "--with-colons"]) From 77bb8d850085f9979422651a0788ac6e46784ef0 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 08:16:35 +0000 Subject: [PATCH 363/397] Make GPG._make_args() shorter and prettier. --- src/gnupg.py | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 642bcfc..5642e81 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -510,23 +510,20 @@ use_agent: %s process. """ ## see TODO file, tag :io:makeargs: - cmd = [self.binary, '--no-emit-version --no-tty --status-fd 2'] + cmd = [self.binary, + '--no-options --no-emit-version --no-tty --status-fd 2'] - if self.homedir: - cmd.append('--homedir "%s"' % self.homedir) + if self.homedir: cmd.append('--homedir "%s"' % self.homedir) if self.keyring: cmd.append('--no-default-keyring --keyring %s' % self.keyring) - if self.secring: - cmd.append('--secret-keyring %s' % self.secring) + if self.secring: + cmd.append('--secret-keyring %s' % self.secring) - if passphrase: - cmd.append('--batch --passphrase-fd 0') + if passphrase: cmd.append('--batch --passphrase-fd 0') - if self.use_agent: - cmd.append('--use-agent') - else: - cmd.append('--no-use-agent') + if self.use_agent: cmd.append('--use-agent') + else: cmd.append('--no-use-agent') if self.options: [cmd.append(opt) for opt in iter(_sanitise_list(self.options))] @@ -534,16 +531,11 @@ use_agent: %s [cmd.append(arg) for arg in iter(_sanitise_list(args))] if self.verbose: - if isinstance(self.verbose, str): - if self.verbose in ['basic', 'advanced', 'expert', 'guru']: - cmd.append('--debug-all') - cmd.append('--debug-level %s' % self.verbose) - elif isinstance(self.verbose, int) and (0 <= self.verbose <= 9): - if self.verbose >= 1: - cmd.append('--debug-all') - cmd.append('--debug-level %s' % self.verbose) - elif self.verbose is True: - cmd.append('--debug-all') + cmd.append('--debug-all') + if ((isinstance(self.verbose, str) and + self.verbose in ['basic', 'advanced', 'expert', 'guru']) + or (isinstance(self.verbose, int) and (1<=self.verbose<=9))): + cmd.append('--debug-level %s' % self.verbose) return cmd From de36fe52d143d57d9eb75a90ca2775c27d041485 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 08:20:27 +0000 Subject: [PATCH 364/397] =?UTF-8?q?Change=20varable=20name=20keyid?= =?UTF-8?q?=E2=86=92default=5Fkey.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gnupg.py | 8 ++++---- tests/test_gnupg.py | 19 ++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 5642e81..2328904 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -710,8 +710,8 @@ use_agent: %s result = None return result - def _sign_file(self, file, keyid=None, passphrase=None, clearsign=True, - detach=False, binary=False): + def _sign_file(self, file, default_key=None, passphrase=None, + clearsign=True, detach=False, binary=False): """Create a signature for a file. :param file: The file stream (i.e. it's already been open()'d) to sign. @@ -737,8 +737,8 @@ use_agent: %s elif detach and not clearsign: args.append("--detach-sign") - if keyid: - args.append(str("--default-key %s" % keyid)) + if default_key: + args.append(str("--default-key %s" % default_key)) result = self._result_map['sign'](self) ## We could use _handle_io here except for the fact that if the diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 0a13394..2039f34 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -530,7 +530,7 @@ class GPGTestCase(unittest.TestCase): """Test that signing a message string works.""" key = self.generate_key("Werner Koch", "gnupg.org") message = "Damn, I really wish GnuPG had ECC support." - sig = self.gpg.sign(message, keyid=key.fingerprint, + sig = self.gpg.sign(message, default_key=key.fingerprint, passphrase='wernerkoch') print "SIGNATURE:\n", sig.data self.assertIsNotNone(sig.data) @@ -539,7 +539,7 @@ class GPGTestCase(unittest.TestCase): """Test that determining the signing algorithm works.""" key = self.generate_key("Ron Rivest", "rsa.com") message = "Someone should add GCM block cipher mode to PyCrypto." - sig = self.gpg.sign(message, keyid=key.fingerprint, + sig = self.gpg.sign(message, default_key=key.fingerprint, passphrase='ronrivest') print "ALGORITHM:\n", sig.sig_algo self.assertIsNotNone(sig.sig_algo) @@ -548,14 +548,15 @@ class GPGTestCase(unittest.TestCase): """Test that signing and verification works.""" key = self.generate_key("Taher ElGamal", "cryto.me") message = 'Ų£ŲµŲ­Ų§ŲØ المصالح لا ŁŠŲ­ŲØŁˆŁ† Ų§Ł„Ų«ŁˆŲ±Ų§ŲŖŲ²' - sig = self.gpg.sign(message, keyid=key.fingerprint, passphrase='foo') + sig = self.gpg.sign(message, default_key=key.fingerprint, + passphrase='foo') self.assertFalse(sig, "Bad passphrase should fail") def test_signature_string_alternate_encoding(self): key = self.generate_key("Nos Oignons", "nos-oignons.net") self.gpg.encoding = 'latin-1' message = "MĆŖle-toi de tes oignons" - sig = self.gpg.sign(message, keyid=key.fingerprint, + sig = self.gpg.sign(message, default_key=key.fingerprint, passphrase='nosoignons') self.assertTrue(sig) @@ -564,7 +565,7 @@ class GPGTestCase(unittest.TestCase): key = self.generate_key("Leonard Adleman", "rsa.com") message_file = os.path.join(_files, 'cypherpunk_manifesto') with open(message_file) as msg: - sig = self.gpg.sign(msg, keyid=key.fingerprint, + sig = self.gpg.sign(msg, default_key=key.fingerprint, passphrase='leonardadleman') self.assertTrue(sig, "I thought I typed my password correctly...") @@ -573,7 +574,7 @@ class GPGTestCase(unittest.TestCase): key = self.generate_key("Bruce Schneier", "schneier.com") message = '...the government uses the general fear of ' message += '[hackers in popular culture] to push for more power' - sig = self.gpg.sign(message, keyid=key.fingerprint, + sig = self.gpg.sign(message, default_key=key.fingerprint, passphrase='bruceschneier') now = time.mktime(time.gmtime()) self.assertTrue(sig, "Good passphrase should succeed") @@ -598,7 +599,7 @@ class GPGTestCase(unittest.TestCase): """Test verfication of an embedded signature.""" key = self.generate_key("Johan Borst", "rijnda.el") message = "You're *still* using AES? Really?" - sig = self.gpg.sign(message, keyid=key.fingerprint, + sig = self.gpg.sign(message, default_key=key.fingerprint, passphrase='johanborst') self.assertTrue(sig, "Good passphrase should succeed") try: @@ -615,7 +616,7 @@ class GPGTestCase(unittest.TestCase): """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, keyid=key.fingerprint, + 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") @@ -638,7 +639,7 @@ class GPGTestCase(unittest.TestCase): """Test that detached signature verification in binary mode fails.""" key = self.generate_key("Adi Shamir", "rsa.com") with open(os.path.join(_files, 'cypherpunk_manifesto'), 'rb') as cm: - sig = self.gpg.sign(cm, keyid=key.fingerprint, + sig = self.gpg.sign(cm, default_key=key.fingerprint, passphrase='adishamir', detach=True, binary=True, clearsign=False) self.assertTrue(sig.data, "File signing should succeed") From 434a650799025e3687e5557c3bf6ded8f538a6ce Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 08:21:25 +0000 Subject: [PATCH 365/397] =?UTF-8?q?Change=20ivar=20name=20p=E2=86=92proc.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gnupg.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 2328904..26b7677 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -740,19 +740,18 @@ use_agent: %s if default_key: args.append(str("--default-key %s" % default_key)) - result = self._result_map['sign'](self) ## We could use _handle_io here except for the fact that if the ## passphrase is bad, gpg bails and you can't write the message. - p = self._open_subprocess(args, passphrase is not None) + result = self._result_map['sign'](self) + proc = self._open_subprocess(args, passphrase is not None) try: - stdin = p.stdin if passphrase: - _util._write_passphrase(stdin, passphrase, self.encoding) - writer = _util._threaded_copy_data(file, stdin) - except IOError: - log.exception("_sign_file(): Error writing message") + _util._write_passphrase(proc.stdin, passphrase, self.encoding) + writer = _util._threaded_copy_data(file, proc.stdin) + except IOError as ioe: + log.exception("Error writing message: %s" % ioe.message) writer = None - self._collect_output(p, result, writer, stdin) + self._collect_output(proc, result, writer, proc.stdin) return result def verify(self, data): From eb93fd48deb8bc70c0b0326e0331ce43d5dccfd8 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 08:22:00 +0000 Subject: [PATCH 366/397] Fix doctest for GPG.import_key(). --- src/gnupg.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 26b7677..de38492 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -818,15 +818,15 @@ use_agent: %s Import the key_data into our keyring. >>> import shutil - >>> shutil.rmtree("keys") - >>> gpg = GPG(homedir="keys") - >>> input = gpg.gen_key_input() - >>> result = gpg.gen_key(input) - >>> print1 = result.fingerprint - >>> result = gpg.gen_key(input) - >>> print2 = result.fingerprint + >>> shutil.rmtree("doctests") + >>> gpg = gnupg.GPG(homedir="doctests") + >>> inpt = gpg.gen_key_input() + >>> key1 = gpg.gen_key(inpt) + >>> print1 = str(key1.fingerprint) >>> pubkey1 = gpg.export_keys(print1) >>> seckey1 = gpg.export_keys(print1,secret=True) + >>> key2 = gpg.gen_key(inpt) + >>> print2 = key2.fingerprint >>> seckeys = gpg.list_keys(secret=True) >>> pubkeys = gpg.list_keys() >>> assert print1 in seckeys.fingerprints @@ -837,14 +837,8 @@ use_agent: %s 'ok' >>> str(gpg.delete_keys(print1)) 'ok' - >>> str(gpg.delete_keys("nosuchkey")) - 'No such key' - >>> seckeys = gpg.list_keys(secret=True) >>> pubkeys = gpg.list_keys() - >>> assert not print1 in seckeys.fingerprints >>> assert not print1 in pubkeys.fingerprints - >>> result = gpg.import_keys('foo') - >>> assert not result >>> result = gpg.import_keys(pubkey1) >>> pubkeys = gpg.list_keys() >>> seckeys = gpg.list_keys(secret=True) @@ -853,10 +847,7 @@ use_agent: %s >>> result = gpg.import_keys(seckey1) >>> assert result >>> seckeys = gpg.list_keys(secret=True) - >>> pubkeys = gpg.list_keys() >>> assert print1 in seckeys.fingerprints - >>> assert print1 in pubkeys.fingerprints - >>> assert print2 in pubkeys.fingerprints """ ## xxx need way to validate that key_data is actually a valid GPG key ## it might be possible to use --list-packets and parse the output From 3516431da983a4c8c8fc3596d3b3f77fd206f410 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 08:22:35 +0000 Subject: [PATCH 367/397] Comment out the pretty printing of results for GPG.import_key() for now. --- src/gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index de38492..33d69f2 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -856,8 +856,8 @@ use_agent: %s log.info('Importing: %r', key_data[:256]) data = _make_binary_stream(key_data, self.encoding) self._handle_io(['--import'], data, result, binary=True) - pretty = pprint(result.__dict__, indent=4, width=76, depth=8) - log.debug("Import result:%s%s" % (os.linesep, pretty)) + #pretty = pprint(result.__dict__, indent=4, width=76, depth=8) + #log.debug("Import result:%s%s" % (os.linesep, pretty)) data.close() return result From ea58623e7527f2b0447a9631db7c7068d74065a8 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 08:23:35 +0000 Subject: [PATCH 368/397] Fix doctest for GPG.recv_keys(). --- src/gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 33d69f2..9cfc666 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -865,8 +865,8 @@ use_agent: %s """Import a key from a keyserver >>> import shutil - >>> shutil.rmtree("keys") - >>> gpg = GPG(homedir="keys") + >>> shutil.rmtree("doctests") + >>> gpg = gnupg.GPG(homedir="doctests") >>> result = gpg.recv_keys('pgp.mit.edu', '3FF0DB166A7476EA') >>> assert result From b6076a7f65e2fed79e54db10be3fce0cc1a0d3ef Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 08:38:18 +0000 Subject: [PATCH 369/397] Add support for using separate keyrings during key generation. * If GPG.gen_key_input() is called with 'separate_keyring = True', then a unique temporary keyring filename will be created in the format: username@hostname_timestamp.[pub|sec]ring where the timestamp is an integer representing seconds since epoch for UTC. The filename for the two temporary keyrings are stored in the class attribute GPG.temp_keyring and GPG.temp_secring, so that when GPG.gen_key() is called with results of GPG.gen_key_input([...], separate_keyring=True), the keys are created using the temporary keyrings, which are then renamed to: fingerprint.[pub|sec]ring where fingerprint is the fingerprint of the key which has just been generated. The attributes GPG.temp_keyring and GPG.temp_secring are then both reset to None, so that these attributes, if the hold anything, only hold the filenames for the last call to GPG.gen_key_input(). * I didn't especially want to add new features right now, but this turned out to be necessary for cases where we want to generate a bunch of keys and then use them right away. Without doing this, it would have been necessary to store the results of the freshly generated key in memory and then write them to a file by hand, which proved to be rather volatile and error-prone, as none of the unittests for encryption/decryption for multiple recipients were passing, nor had ever passed. (A note again on the latter: upstream's unittests for encryption and decryption are entirely encased in try/except blocks which CATCH ALL ERRORS. Qu'est-que fuck is the point of a unittest if it catches all errors?) --- src/gnupg.py | 87 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 67 insertions(+), 20 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 9cfc666..a6daa02 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1065,9 +1065,33 @@ use_agent: %s f = _make_binary_stream(input, self.encoding) self._handle_io(args, f, key, binary=True) f.close() + + fpr = str(key.fingerprint) + if len(fpr) == 20: + if self.temp_keyring or self.temp_secring: + if not os.path.exists(self._key_dir): + os.makedirs(self._key_dir) + prefix = os.path.join(self._key_dir, fpr) + + if self.temp_keyring: + if os.path.isfile(self.temp_keyring): + try: os.rename(self.temp_keyring, prefix+".pubring") + except OSError as ose: log.error(ose.message) + else: self.temp_keyring = None + #finally: self.import_keys(fpr) + + if self.temp_secring: + if os.path.isfile(self.temp_secring): + try: os.rename(self.temp_secring, prefix+".secring") + except OSError as ose: log.error(ose.message) + else: self.temp_secring = None + #finally: self.import_keys(fpr) + + log.info("Key created. Fingerprint: %s" % fpr) return key - def gen_key_input(self, save=False, testing=False, **kwargs): + def gen_key_input(self, separate_keyring=False, save_batchfile=False, + testing=False, **kwargs): """Generate a batch file for input to :meth:`GPG.gen_key()`. The GnuPG batch file key generation feature allows unattended key @@ -1129,6 +1153,22 @@ use_agent: %s >>> encrypted = gpg.encrypt(message, alice_key.fingerprint) >>> assert isinstance(encrypted.data, str) + :param bool separate_keyring: Specify for the new key to be written to + a separate pubring.gpg and + secring.gpg. If True, + :meth:`GPG.gen_key` will automatically + rename the separate keyring and secring + to whatever the fingerprint of the + generated key ends up being, suffixed + with '.pubring' and '.secring' + respectively. + + :param bool save_batchfile: Save a copy of the generated batch file to + disk in a file named .batch, + where is the ``name_real`` + parameter stripped of punctuation, spaces, + and non-ascii characters. + :param bool testing: Uses a faster, albeit insecure random number generator to create keys. This should only be used for testing purposes, for keys which are @@ -1136,11 +1176,6 @@ use_agent: %s destroyed, and never for the generation of actual use keys. - :param bool save: Save a copy of the generated batch file to disk in a - file named .batch, where is - the ``name_real`` parameter stripped of punctuation, - spaces, and non-ascii characters. - :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_email: The email in the UID of the generated key. @@ -1265,16 +1300,17 @@ use_agent: %s parms = {} + #: A boolean for determining whether to set subkey_type to 'default' + default_type = False + + name_email = kwargs.get('name_email') + uidemail = _util.create_uid_email(name_email) + parms.setdefault('Key-Type', 'default') parms.setdefault('Key-Length', 4096) parms.setdefault('Name-Real', "Autogenerated Key") parms.setdefault('Expire-Date', _util._next_year()) - - try: logname = os.environ['LOGNAME'] - except KeyError: logname = os.environ['USERNAME'] - hostname = socket.gethostname() - uidname = "%s@%s" % (logname.replace(' ', '_'), hostname) - parms.setdefault('Name-Email', uidname) + parms.setdefault('Name-Email', uidemail) if testing: ## This specific comment string is required by (some? all?) @@ -1285,15 +1321,14 @@ use_agent: %s key = key.replace('_','-').title() ## to set 'cert', 'Key-Usage' must be blank string if not key in ('Key-Usage', 'Subkey-Usage'): - ## otherwise skip anything else that's empty if str(val).strip(): parms[key] = val ## if Key-Type is 'default', make Subkey-Type also be 'default' if parms['Key-Type'] == 'default': - subkey_must_be_default = True + default_type = True for field in ('Key-Usage', 'Subkey-Usage',): - try: parms.pop(field) ## usage shouldn't be specified + try: parms.pop(field) ## toss these out, handle manually except KeyError: pass ## Key-Type must come first, followed by length @@ -1302,7 +1337,7 @@ use_agent: %s if 'Subkey-Type' in parms.keys(): out += "Subkey-Type: %s\n" % parms.pop('Subkey-Type') else: - if subkey_must_be_default: + if default_type: out += "Subkey-Type: default\n" if 'Subkey-Length' in parms.keys(): out += "Subkey-Length: %s\n" % parms.pop('Subkey-Length') @@ -1310,8 +1345,20 @@ use_agent: %s for key, val in list(parms.items()): out += "%s: %s\n" % (key, val) - out += "%%pubring %s\n" % self.keyring - out += "%%secring %s\n" % self.secring + ## There is a problem where, in the batch files, if the '%%pubring' + ## and '%%secring' are given as any static string, i.e. 'pubring.gpg', + ## that file will always get rewritten without confirmation, killing + ## off any keys we had before. So in the case where we wish to + ## generate a bunch of keys and then do stuff with them, we should not + ## give 'pubring.gpg' as our keyring file, otherwise we will lose any + ## keys we had previously. + + if separate_keyring: + ring = str(uidemail + '_' + str(_util._utc_epoch())) + self.temp_keyring = os.path.join(self.homedir, ring+'.pubring') + self.temp_secring = os.path.join(self.homedir, ring+'.secring') + out += "%%pubring %s\n" % self.temp_keyring + out += "%%secring %s\n" % self.temp_secring if testing: ## see TODO file, tag :compatibility:gen_key_input: @@ -1323,7 +1370,7 @@ use_agent: %s out += "%commit\n" ## if we've been asked to save a copy of the batch file: - if save and parms['Name-Email'] != uidname: + if save_batchfile and parms['Name-Email'] != uidemail: asc_uid = encodings.normalize_encoding(parms['Name-Email']) filename = _fix_unsafe(asc_uid) + _util._now() + '.batch' save_as = os.path.join(self._batch_dir, filename) @@ -1339,7 +1386,7 @@ use_agent: %s if getattr(self.gen_key_input, '__doc__', None) is not None: docs = self.gen_key_input.__doc__ else: - docs = str() ## no docstrings if run with "python -OO" + docs = str() ## docstring=None if run with "python -OO" links = '\n'.join(x.strip() for x in docs.splitlines()[-2:]) explain = """ This directory was created by python-gnupg, on {}, and From 897f32307e4a65d2319152733d063f1e4d656b09 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 08:52:35 +0000 Subject: [PATCH 370/397] Make the kwargs for GPG.encrypt_file() appear slightly less overwhelming. --- src/gnupg.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index a6daa02..31cc284 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1402,10 +1402,16 @@ generate keys. Please see return out - def encrypt_file(self, filename, recipients, default_key=None, - always_trust=True, passphrase=None, armor=True, - output=None, encrypt=True, symmetric=False, - cipher_algo='AES256', digest_algo='SHA512', + def encrypt_file(self, filename, recipients, + default_key=None, + passphrase=None, + armor=True, + encrypt=True, + symmetric=False, + always_trust=True, + output=None, + cipher_algo='AES256', + digest_algo='SHA512', compress_algo='ZLIB'): """Encrypt the message read from ``file``. From 8b355d5541bdab321571ff43e63fa6f1f6986a78 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 08:59:10 +0000 Subject: [PATCH 371/397] Update docstring for GPG.encrypt_file() with new parameter type requirements. * GPG.encrypt_file() isn't ever directly used now, and so we should switch to calling it in the same way that GPG._sign_file() is now called when necessary though GPG.sign(). --- src/gnupg.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 31cc284..8c130f3 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1413,24 +1413,25 @@ generate keys. Please see cipher_algo='AES256', digest_algo='SHA512', compress_algo='ZLIB'): - """Encrypt the message read from ``file``. + """Encrypt the message read from the file-like object ``filename``. :param str filename: The file or bytestream to encrypt. - :type recipients: str or list or tuple - :param recipients: The recipients to encrypt to. Recipients may be - specified by UID or keyID/fingerprint. - :param str default_key: The keyID to use for signing, i.e. - "gpg --default-key A3ADB67A2CDB8B35 --sign ..." + :param str recipients: The recipients to encrypt to. Recipients must + be specified keyID/fingerprint. Care should be + taken in Python2.x to make sure that the given + fingerprint is in fact a string and not a + unicode object. + :param str default_key: The keyID/fingerprint of the key to use for + signing. If given, ``filename`` will be + encrypted and signed. :param bool always_trust: If True, ignore trust warnings on recipient keys. If False, display trust warnings. (default: True) - :param bool passphrase: If True, use the stored passphrase for our - secret key. - - :param bool armor: If True, ascii armor the encrypted output; if False, - the encrypted output will be in binary - format. (default: True) - + :param str passphrase: If True, use this passphrase to unlock the + secret portion of the ``default_key`` for + signing. + :param bool armor: If True, ascii armor the output; otherwise, the + output will be in binary format. (default: True) :param str output: The output file to write to. If not specified, the encrypted output is returned, and thus should be stored as an object in Python. For example: From 7c9c00a9579f20972d17e739bebf17a9fa39ad44 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:01:48 +0000 Subject: [PATCH 372/397] Rewrite encrypt_file() to handle unicode/str, fn/files, and multi recipients. --- src/gnupg.py | 92 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 60 insertions(+), 32 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index 8c130f3..a3f8523 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1436,49 +1436,77 @@ generate keys. Please see encrypted output is returned, and thus should be stored as an object in Python. For example: """ - args = list() + args = [] - ## both can be used at the same time for an encrypted file which - ## is decryptable with a passphrase or secretkey. - if encrypt: - args.append('--encrypt') - if symmetric: - args.append('--symmetric') - - if not _util._is_list_or_tuple(recipients): - if isinstance(recipients, str): - recipients = [rec for rec in recipients.split(' ')] - else: - recipients = (recipients,) - if len(recipients) > 1: - args.append('--multifile') - for recipient in recipients: - args.append('--recipient %s' % recipient) - - if output is not None: + if output: if getattr(output, 'fileno', None) is not None: - if os.path.exists(output): - os.remove(output) # to avoid overwrite confirmation message - args.append('--output "%s"' % output) + ## avoid overwrite confirmation message + if getattr(output, 'name', None) is None: + if os.path.exists(output): + os.remove(output) + args.append('--output %s' % output) + else: + if os.path.exists(output.name): + os.remove(output.name) + args.append('--output %s' % output.name) + + if armor: args.append('--armor') + if always_trust: args.append('--always-trust') + if cipher_algo: args.append('--cipher-algo %s' % cipher_algo) + if compress_algo: args.append('--compress-algo %s' % compress_algo) - if armor: - args.append('--armor') if default_key: args.append('--sign') args.append('--default-key %s' % default_key) if digest_algo: args.append('--digest-algo %s' % digest_algo) - if always_trust: - args.append('--always-trust') - if cipher_algo: - args.append('--cipher-algo %s' % cipher_algo) - if compress_algo: - args.append('--compress-algo %s' % compress_algo) + ## both can be used at the same time for an encrypted file which + ## is decryptable with a passphrase or secretkey. + if symmetric: args.append('--symmetric') + if encrypt: args.append('--encrypt') + + if len(recipients) >= 1: + log.debug("GPG.encrypt() called for recipients '%s' with type '%s'" + % (recipients, type(recipients))) + + if isinstance(recipients, (list, tuple)): + for recp in recipients: + if not _util._py3k: + if isinstance(recp, unicode): + try: + assert _parsers._is_hex(str(recp)) + except AssertionError: + log.info("Can't accept recipient string: %s" + % recp) + else: + args.append('--recipient %s' % str(recp)) + continue + ## will give unicode in 2.x as '\uXXXX\uXXXX' + args.append('--recipient %r' % recp) + continue + if isinstance(recp, str): + args.append('--recipient %s' % recp) + + elif (not _util._py3k) and isinstance(recp, basestring): + for recp in recipients.split('\x20'): + args.append('--recipient %s' % recp) + + elif _util._py3k and isinstance(recp, str): + for recp in recipients.split(' '): + args.append('--recipient %s' % recp) + ## ...and now that we've proven py3k is better... + + else: + log.debug("Don't know what to do with recipients: '%s'" + % recipients) result = self._result_map['crypt'](self) - self._handle_io(args, file, result, passphrase=passphrase, binary=True) - log.debug('GPG.encrypt(): Result: %r', result.data) + log.debug("Got filename '%s' with type '%s'." + % (filename, type(filename))) + self._handle_io(args, filename, result, + passphrase=passphrase, binary=True) + log.debug('GPG.encrypt_file(): Result: %r', result.data) return result def encrypt(self, data, recipients, **kwargs): From bfbb189459b7e97b0d2e7c59c684f42b35afd9f4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:04:15 +0000 Subject: [PATCH 373/397] GPG.encrypt() should now be called with each recipient as a single parameter. --- src/gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index a3f8523..3074d97 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1509,7 +1509,7 @@ generate keys. Please see log.debug('GPG.encrypt_file(): Result: %r', result.data) return result - def encrypt(self, data, recipients, **kwargs): + def encrypt(self, data, *recipients, **kwargs): """Encrypt the message contained in ``data`` to ``recipients``. >>> import shutil From 688ceb9a6dda0633d3e2afd9fc4f896861f7c065 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:06:17 +0000 Subject: [PATCH 374/397] =?UTF-8?q?Change=20parameter=20name=20data?= =?UTF-8?q?=E2=86=92message=20in=20GPG.decrypt().?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gnupg.py b/src/gnupg.py index 3074d97..f2728b6 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1560,7 +1560,7 @@ generate keys. Please see :param message: A string or file-like object to decrypt. """ - stream = _make_binary_stream(data, self.encoding) + stream = _make_binary_stream(message, self.encoding) result = self.decrypt_file(stream, **kwargs) stream.close() return result From 490574c3ec576c62a5d8e6039a8313b5f9e950e8 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:08:49 +0000 Subject: [PATCH 375/397] =?UTF-8?q?Change=20variable=20name=20file?= =?UTF-8?q?=E2=86=92filename=20in=20GPG.decrypt=5Ffile().?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gnupg.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gnupg.py b/src/gnupg.py index f2728b6..7158ff5 100644 --- a/src/gnupg.py +++ b/src/gnupg.py @@ -1565,7 +1565,7 @@ generate keys. Please see stream.close() return result - def decrypt_file(self, file, always_trust=False, passphrase=None, + def decrypt_file(self, filename, always_trust=False, passphrase=None, output=None): """ Decrypt the contents of a file-like object :param:file . @@ -1579,11 +1579,11 @@ generate keys. Please see if output: # write the output to a file with the specified name if os.path.exists(output): os.remove(output) # to avoid overwrite confirmation message - args.append('--output "%s"' % output) + args.append('--output %s' % output) if always_trust: args.append("--always-trust") result = self._result_map['crypt'](self) - self._handle_io(args, file, result, passphrase, binary=True) + self._handle_io(args, filename, result, passphrase, binary=True) log.debug('decrypt result: %r', result.data) return result From 611184f242e69e1af3f29c03b9945f7dc617269b Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:09:32 +0000 Subject: [PATCH 376/397] Add '--no-options' to unittest on _make_options() output. --- tests/test_gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 2039f34..58b56f0 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -269,7 +269,7 @@ class GPGTestCase(unittest.TestCase): self.gpg.options = ['--tyrannosaurus-rex', '--stegosaurus'] cmd = self.gpg._make_args(None, False) expected = ['/usr/bin/gpg', - '--no-emit-version --no-tty --status-fd 2', + '--no-options --no-emit-version --no-tty --status-fd 2', '--homedir "%s"' % self.homedir, '--no-default-keyring --keyring %s' % self.keyring, '--secret-keyring %s' % self.secring, From dad755ec3a178214fe48b164f91843f7d56c0677 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:10:14 +0000 Subject: [PATCH 377/397] Awkwardly, fingerprints need to always be strings. Change unittests to do so. --- tests/test_gnupg.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 58b56f0..9046e63 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -662,10 +662,10 @@ class GPGTestCase(unittest.TestCase): """Test encryption of a message string""" key = self.generate_key("Craig Gentry", "xorr.ox", passphrase="craiggentry") - gentry = key.fingerprint + gentry = str(key.fingerprint) key = self.generate_key("Marten van Dijk", "xorr.ox", passphrase="martenvandijk") - dijk = key.fingerprint + dijk = str(key.fingerprint) gpg = self.gpg message = """ In 2010 Riggio and Sicari presented a practical application of homomorphic @@ -683,9 +683,9 @@ authentication.""" """Test encryption with latin-1 encoding""" key = self.generate_key("Craig Gentry", "xorr.ox", passphrase="craiggentry") - gentry = key.fingerprint + gentry = str(key.fingerprint) key = self.generate_key("Marten van Dijk", "xorr.ox") - dijk = key.fingerprint + dijk = str(key.fingerprint) self.gpg.encoding = 'latin-1' if _util._py3k: data = 'Hello, AndrĆ©!' From a9ea5a1e48516f0ecd91155b4b531391b61a75d8 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:13:55 +0000 Subject: [PATCH 378/397] Change unittest because recipients don't needs to be lists/tuples anymore. --- tests/test_gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 9046e63..9e8908f 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -674,7 +674,7 @@ transparent multi-hop wireless backhauls that are able to perform statistical analysis of different kinds of data (temperature, humidity, etc.) coming from a WSN while ensuring both end-to-end encryption and hop-by-hop authentication.""" - encrypted = str(gpg.encrypt(message, [dijk], )) + encrypted = str(gpg.encrypt(message, dijk)) self.assertNotEqual(message, encrypted) self.assertNotEqual(encrypted, '') self.assertGreater(len(encrypted), 0) From 2c577466c35d4925ec966cef4d695f52206fe322 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:14:38 +0000 Subject: [PATCH 379/397] Add two helpful debug statements, remove two superfluous asserts from unittest. --- tests/test_gnupg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 9e8908f..f08c863 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -675,9 +675,9 @@ analysis of different kinds of data (temperature, humidity, etc.) coming from a WSN while ensuring both end-to-end encryption and hop-by-hop authentication.""" encrypted = str(gpg.encrypt(message, dijk)) + log.debug("Plaintext: %s" % message) + log.debug("Encrypted: %s" % encrypted) self.assertNotEqual(message, encrypted) - self.assertNotEqual(encrypted, '') - self.assertGreater(len(encrypted), 0) def test_encryption_alt_encoding(self): """Test encryption with latin-1 encoding""" From c4ab6bd5d69e896a166feb96a13e9eb9b71c6029 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:17:27 +0000 Subject: [PATCH 380/397] Change unittest, recipients don't need to be lists/tuples. --- tests/test_gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index f08c863..abbe60a 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -692,7 +692,7 @@ authentication.""" else: data = unicode('Hello, AndrĆ©', self.gpg.encoding) data = data.encode(self.gpg.encoding) - encrypted = str(self.gpg.encrypt(data, [gentry])) + encrypted = self.gpg.encrypt(data, gentry) self.assertNotEqual(data, encrypted) self.assertNotEqual(encrypted, '') self.assertGreater(len(encrypted), 0) From 70ceb50744d6a199b77033a58eaa9736654aa704 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:18:13 +0000 Subject: [PATCH 381/397] Fix a unittest so that the data returned is a human readable string. --- tests/test_gnupg.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index abbe60a..2dd79dc 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -693,9 +693,9 @@ authentication.""" data = unicode('Hello, AndrĆ©', self.gpg.encoding) data = data.encode(self.gpg.encoding) encrypted = self.gpg.encrypt(data, gentry) - self.assertNotEqual(data, encrypted) - self.assertNotEqual(encrypted, '') - self.assertGreater(len(encrypted), 0) + edata = str(encrypted.data) + self.assertNotEqual(data, edata) + self.assertGreater(len(edata), 0) def test_encryption_multi_recipient(self): """Test encrypting a message for multiple recipients""" From 348c5b59fbe61cac12f33afb6caf9cb5ce8811fc Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:20:51 +0000 Subject: [PATCH 382/397] If setting Key-Length, also set Subkey-Length, else GnuPG defaults to 1024. --- tests/test_gnupg.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 2dd79dc..f2a50b5 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -702,15 +702,25 @@ authentication.""" ian = { 'name_real': 'Ian Goldberg', 'name_email': 'gold@stein', 'key_type': 'RSA', + 'key_length': 2048, 'key_usage': '', 'subkey_type': 'RSA', + 'subkey_length': 2048, 'subkey_usage': 'encrypt,sign', 'passphrase': 'victorygin' } + + ## when we don't specify the subkey lengths and the keylength + ## gets set automatically in gen_key_input(), gpg complains: + ## + ## gpg: keysize invalid; using 1024 bits + ## kat = { 'name_real': 'Kat Hannah', 'name_email': 'kat@pics', 'key_type': 'RSA', + 'key_length': 2048, 'key_usage': '', 'subkey_type': 'RSA', + 'subkey_length': 2048, 'subkey_usage': 'encrypt,sign', 'passphrase': 'overalls' } From 622544ef138b73e65a980e2cd5b55b4d74a123d4 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:23:32 +0000 Subject: [PATCH 383/397] Rewrite test_encryption_multi_recipient to also test separate keyrings option. --- tests/test_gnupg.py | 42 +++++++++++++++++------------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index f2a50b5..7fcfee8 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -699,6 +699,8 @@ authentication.""" def test_encryption_multi_recipient(self): """Test encrypting a message for multiple recipients""" + self.gpg.homedir = _here + ian = { 'name_real': 'Ian Goldberg', 'name_email': 'gold@stein', 'key_type': 'RSA', @@ -724,31 +726,16 @@ authentication.""" 'subkey_usage': 'encrypt,sign', 'passphrase': 'overalls' } - ian_input = self.gpg.gen_key_input(**ian) - kat_input = self.gpg.gen_key_input(**kat) - + ian_input = self.gpg.gen_key_input(separate_keyring=True, **ian) ian_key = self.gpg.gen_key(ian_input) + log.debug("ian_key status: %s" % ian_key.status) + ian_fpr = str(ian_key.fingerprint) + + kat_input = self.gpg.gen_key_input(separate_keyring=True, **kat) kat_key = self.gpg.gen_key(kat_input) + log.debug("kat_key status: %s" % kat_key.status) + kat_fpr = str(kat_key.fingerprint) - log.debug("ian_key.status=%s" % ian_key.status) - log.debug("kat_key.status=%s" % kat_key.status) - - import pdb - pdb.set_trace() - - #self.assertIsNotNone(str(ian_key.fingerprint)) - #self.assertIsNotNone(str(kat_key.fingerprint)) - - #self.assertTrue(ian_key.primary_created) - #self.assertTrue(ian_key.subkey_created) - - #self.assertTrue(kat_key.primary_created) - #self.assertTrue(kat_key.subkey_created) - - ian_fpr = ian_key.fingerprint - kat_fpr = kat_key.fingerprint - - gpg = self.gpg message = """ In 2010 Riggio and Sicari presented a practical application of homomorphic encryption to a hybrid wireless sensor/mesh network. The system enables @@ -756,10 +743,15 @@ transparent multi-hop wireless backhauls that are able to perform statistical analysis of different kinds of data (temperature, humidity, etc.) coming from a WSN while ensuring both end-to-end encryption and hop-by-hop authentication.""" - encrypted = gpg.encrypt(message, [ian_fpr, kat_fpr]) + + log.debug("kat_fpr type: %s" % type(kat_fpr)) + log.debug("ian_fpr type: %s" % type(ian_fpr)) + + encrypted = self.gpg.encrypt(message, (ian_fpr, kat_fpr)) + log.debug("Plaintext: %s" % message) + log.debug("Ciphertext: %s" % str(encrypted.data)) + self.assertNotEqual(message, str(encrypted.data)) - self.assertNotEqual(str(encrypted.data), '') - self.assertGreater(len(str(encrypted.data)), 0) def test_decryption(self): """Test decryption""" From c1fb532c530faec439ea783130265fd364327764 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:25:34 +0000 Subject: [PATCH 384/397] Change unittest, recipients doesn't need to be list, and data should be string. --- tests/test_gnupg.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 7fcfee8..6bc9727 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -773,14 +773,14 @@ analysis of different kinds of data (temperature, humidity, etc.) coming from a WSN while ensuring both end-to-end encryption and hop-by-hop authentication.""" - encrypted = self.gpg.encrypt(message, [ruck_fpr,]) - decrypted = self.gpg.decrypt(encrypted.data, passphrase="ruck") + encrypted = str(self.gpg.encrypt(message, ruck_fpr)) + decrypted = str(self.gpg.decrypt(encrypted, passphrase="ruck")) - if message != decrypted.data: - log.debug("was: %r", message) - log.debug("new: %r", decrypted.data) + if message != decrypted: + log.debug("was: %r" % message) + log.debug("new: %r" % decrypted) - self.assertEqual(message, decrypted.data) + self.assertEqual(message, decrypted) def test_decryption_multi_recipient(self): """Test decryption of an encrypted string for multiple users""" From 690b0efc763a7655a736f1673440c3993646f784 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:27:12 +0000 Subject: [PATCH 385/397] Rewrite unittest for encryption/decryption to multi recipients. --- tests/files/test_key_1.pub | 55 +++++++++++++++++++++++++++ tests/files/test_key_1.sec | 78 ++++++++++++++++++++++++++++++++++++++ tests/files/test_key_2.pub | 48 +++++++++++++++++++++++ tests/files/test_key_2.sec | 77 +++++++++++++++++++++++++++++++++++++ tests/test_gnupg.py | 56 +++++++++++++++++++++------ 5 files changed, 302 insertions(+), 12 deletions(-) create mode 100644 tests/files/test_key_1.pub create mode 100644 tests/files/test_key_1.sec create mode 100644 tests/files/test_key_2.pub create mode 100644 tests/files/test_key_2.sec diff --git a/tests/files/test_key_1.pub b/tests/files/test_key_1.pub new file mode 100644 index 0000000..3b48882 --- /dev/null +++ b/tests/files/test_key_1.pub @@ -0,0 +1,55 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.12 (GNU/Linux) + +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 +=sTVP +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/files/test_key_1.sec b/tests/files/test_key_1.sec new file mode 100644 index 0000000..d02c7dd --- /dev/null +++ b/tests/files/test_key_1.sec @@ -0,0 +1,78 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.12 (GNU/Linux) + +lQEVBFGh57gBCADRG2Q93/INxPOALefgXj40I8+i9Sc7MblIbZeu3ctAQUL41OXw +zdTo2NKaDH+CcIQrlQZfMnchYnr69M1mDcj2MQy6DqixvNrwJKF4CZwrmFpZRH6W +1NJ5yeU4Q6e0kAXZCSJDFjRaWv68HaNpW8j4XTHdcwmv+ry6APBa+mwhXXqAYmnS +EFu6uk7tEjebnYjnP+J6XxBSHmF7678bvWW7M5IQkG/zxnWpsfLCcrakpxMAJnhZ +EDhZggAl5Rvpd008H6ERJSgLWdZ03qLUutRX0OaJdH/v/aDLDxclYVZtnC1Kg3Jy +2WU+QRP2q2q+xNSJ+smRE1wp+BnMz7qiyt8VABEBAAH+A2UCR05VAbQ5cHl0aG9u +LWdudXBnIHRlc3Qga2V5ICMxIChETyBOT1QgVVNFKSA8dGVzdEBweXRob24tZ251 +cGc+iQE+BBMBAgAoBQJRoee4AhsBBQkB4TOABgsJCAcDAgYVCAIJCgsEFgIDAQIe +AQIXgAAKCRCtsugm/7ujSMduCACnfxHLgOJHlfUl9H0VTtccVZetq4W3vSz663zB +7EI3T0UJLK6YlaMCJoraN2mNR20DB26j+vnrgAzV+tQO0cZJkJtZ6zej0IIRAkOa +qjYQKF1QC4gqK07dSHUJJV73CevYyOKofZhfFwly8GckdE5CY6eXdmEU+2pUMpvd +j7ZGh+fOE9zsbsyvU7QSj4nJeRHFsuoiGRw4Rri97mUpnyVc35lgzEvnO4RpeeVS +PBZhxX/8p2flXdGi0WKb4T7W5GVxyXow9HzgGPc/55uy19NOtGLLHqB3R77847ly +BELTV9+oz5mGylR9I463LOS8kLAhMEadwhPdspOxUDww0gF2nQO+BFGh6CsBCACx +d4Sic2mFp9u5rC3LVLY7M01W75uQ3XhFtE/WT+tQ2uTmXF1191AFtC+EB72CK5QU +4nohlNmKBK3c+TshFPYXSQ3qy8KwaKE6b8hdFOEYlgv3E+WF/rRIKrhVde4hlkyu +8GLNBwWXCOIeug0dTDaiKXjLpX5wmK/Pu7btmF0wF9EpKGkiJouVEZrPS5zglM61 +wsd9Ka/miswKQEt0gNcaVuz1VY18E5dBKQi4iYuh/EySkfbPAzjZk/OxqEBiADE8 +nGULDalOYXRUEWRhVGr+nmdV6jXx7TeclitJf1tOGXl4rcJ90T1csR1t/Vv/SOTC +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 +m8XB+to= +=GLJA +-----END PGP PRIVATE KEY BLOCK----- diff --git a/tests/files/test_key_2.pub b/tests/files/test_key_2.pub new file mode 100644 index 0000000..e3bd904 --- /dev/null +++ b/tests/files/test_key_2.pub @@ -0,0 +1,48 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.12 (GNU/Linux) + +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 +=bZbj +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/files/test_key_2.sec b/tests/files/test_key_2.sec new file mode 100644 index 0000000..7f113c5 --- /dev/null +++ b/tests/files/test_key_2.sec @@ -0,0 +1,77 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1.4.12 (GNU/Linux) + +lQEVBFGh6pQBCADU4GbfWdaVQGVJ+qJn1iT6uUe10GRMzCot5Cp5T/Md7B0E8JU+ +K14igCLdCtJU/m5D4hpVoEEj+uELvNm/5OzTmMhL7c8vREBdMa/3uVmDUVuHpO6B +93+9249aBEwj79a17kp615Lyc7dz8xIYTCjWAkVEMGDfTlSVw2xs+qYUkrbXLy/a +9sMPnHo8WVhqG8iGszmAPP1mIzYEyUN8fuxi/MLMnkpV5h3q8Qon44tzjfj7JsKj +3P6jcA0NteF6pZcKOK82sRKjID9mJgl1nHwSvtpz+AHqdULlHnJ8QuTZCk4zCrSn +rcpQMg5biJ3XLrGIcgwPNQV9xhfyWIbUvSbvABEBAAH+A2UCR05VAbQtdGVzdCBr +ZXkgIzIgKERPIE5PVCBVU0UpIDx0ZXN0MkBweXRob24tZ251cGc+iQE+BBMBAgAo +BQJRoeqUAhsBBQkB4TOABgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRDk6BMj +55VO0D5qCADIBCDIeaFb3GywBUHmMr9rqG80y7zmGrmH+D5GlO3a/OP8yUq80AUZ +Sx5bgL0vkalowCh63c3Y8Xj9i5J9YbrjboAbojrxRxy4OlmqkDu85Rb0F6RilAHE +14vbl1vnFS8AiiGoHQJ3rNTIcULWq/fbrndl07zntgl1J8FDlpUz2Uabg+52MpGY +XUmY/DQdStr0f0+V5IpzbwEyP6SNGQlb8g4lgnMKydmpt+6P9as7kaIEtVuZqiCG +cCl8WdQh7IHGnhKSaOnovGgt1sb1w4JZDkYWybZDD5/tiv56S760xCaIKUJRPh22 +kmW9v5t4syw0og7TzWHjtkvIVN36x0cwnQO+BFGh6w8BCADGqWdSNxgIVuEVdyf3 +cpayXL5ZoztOzbQ9u/VNQFDrOvrrRDBfVAul+a7C1QqCMVGvkZYqUotI2Gx25bK1 +YBryve1s3mbZC0CJ+zlW/JBJLGvLPgrbp4F6M/qxgK2W8d5M9HU7EQampdetn0Cr +x7IP8a30HkF+WKrXc6XBmGQEPgjguHDisq9TUzxG6FZ1+ycspz/eepLQK1kxfPzW +itg6S8Wu4mFNlhwnbCXWXJRgElvh5vM569wt/BmdX89wnGK/k36/N4yZZutbpbCV +20zo13l7r3GCzweoueDSCvO9TNNDwsAnKwDjoGxauzrV87DM1g/RzDM/6Vrgb46N +32eTABEBAAH+AwMCkHk0gj7hSzxgh8nBL+SOKeRQxo2bP+3SF/IpSjFgvmOELvfe +IVGL5UHXZ5593wI6uICnM6+uqY6pmABvnsJ+Q5GNI2q1fRijaQQiNit0YlieZOhE +rkaBGWwcFKwa6WwSkYyLBw6b+RPywjCpKHcV0FZ7JjUyeKXmiHNRqpWkwrv0izXO +cXAMwts+ccAaqLvBFNuPSmY3071iQ5dVvfDgLehhTStlMZ8mDL7tQchIDmQao7Ni +QXGT419ldX3q4cgVkj0AjZQdIfsMSyvwqMs6TmKRJkDROVyAVxtXb3eE0HK7ssJa +9geF3xMHbeDe3pDaMFqsPQjkiiM0u7XvYIAtCtbYMzk66RcYHLgDebG8jkYTkpqk +P7A5BG5y0aNKi85ub0A0jU1VJMBY+JI64P2E6CXjy1q11nitMlmJukVBfWzKabml +0J2ulYxzRBiWfJIxatDiGp646iWzrVIrOpx6oLE4OUhuRe2i7XTnkfcCWGFO1MZW +yUqBJnspIdESHs+j9hxmUtnIPtmrBLpd2fmkXakYFnu+e2tDQBwTOzMPGQ19u/1U +2LNGZo0CVDNCzfmnm7WEl9UxIk4kjP3zomfAv2XjVQQPQUMqt1qwpJVyKfJsdAdE +CUtsaXkiRbeLvdSR+nVoXDET+ZDZKYTdbKvFsnzkJDRhpEb1aHZEBkdY2ZOanQbK +hfczYIwnDuhckp2TUYW5HW0eKh9PA1jJjhsFH+LZdDVkQ2knJfkn76OJnAumkohE +9DKgHq3OEpeMa/oTE0tJBHcufh+l89R5D7Og3731EBVsJwAmtOIfQtgbVzD0r1hi +EI0d1gfpjjHRZZKC6msqx9xbmDa5owVV4UPmkSVv/hxuw8mY3V7wk9pML7vzIHhl +LnoucJ1BnXlkXF23Yj9OV+bvdtqWbsIuIokBJQQYAQIADwUCUaHrDwIbDAUJAeEz +gAAKCRDk6BMj55VO0KeuB/4iCe4FmUG4RZO9M9qLwZ+xw9zCP3yV/HBOCwAXnMkG +P0h0zrf6nna0vbdQPFDt5PqoytuI+WaLoYqVaMCD99OBAfG4CXK0JTkX65GHNL+K +Y1rjC4PN/2rC1NB/cIFT4dBXh+KtK3lGDbTqYL99+J6vtOwx15hPjLtb2gBMLU0k +AiBhe8gzUxsTdjbq/lY4+37bGXHvCBGeUXZNh+NSBa/ifGoCF8HQi54l+HnQpJXM +B6RopLtMYN9VTVbCQY7itSy2JR7RUxurJXFdv9Bsot3tHGwzemhUOl0Z4R+SlC9R +c2pzMOQrAyqR1ckhbbwOGlS0zzNPhugftxIuMSubz4pUnQO+BFGh6y8BCAC+hr3V +7ARlTLEQDT9pfkYDR6VDF/o9H74b2BSnlQbOsx7IWuHvvIHEm07naytZSum/Kiha +HawT18w7LQP3V/gIwMalqhx+fE+9ppcsQ6a/prahDPnTm2q3KLM2+7SEDZ5KA8Jm +b/5F5pW60C2Vt2gl9XOOpCHKwqguvIgzrjbypqEu0GiqsZPqQtF7MqdNobHYGj93 +HnNf8Inz0I1hfO2ImV6ksb7+0TzxTjHQhMaKhEUCP7kzwnSUqjm92LmMouSvoP8B +p4qw/V/ZOsD1v1ImGWcKKQzKpfDn57hD8t6UP7MPH96OzIqZhvIdlX+PNm5SbhX3 +UAMLql21ZeK1UwgzABEBAAH+AwMCy7jrCEcZti9gszC7JWKa9anp7NZyBh7U25fA +NmrAQfSpcrfHW/HIjpbb3xISAlD8bdDs5PGIAwQ7v9a1hdEqcQ15JNM5WJaTfEhq +Ox5iEHkHjWJnrYDTel9FVMqwnsXvjcKY+0xcp1FB/xtRk0o03rZDD+GWWyyw6tAt +tvB2KLPr0+Ud8ea7w6RVbip3hzbRi5g1x9HlFKkdSu4CjtOLjR/ama4+zAvLJ/h+ +3gQU7Z8Z5fz5OgOenGXxmZVFkhsfP6gRoZWri3QgWiFcnIfRp7BlQRx6c1W0alLH +GRAv4YAU4fmDy1QgbtJj+3OLw6vNKS7kC5LCk9k+FjaBymKQ7o6Orj08xB6Xj0Uy +Z2BqJuVCP4f6wjQMtEcO/+Ij+e61GzF1vTRTJ55dFA/yELjpnFVkcn8knWQbtsIw +fgEUazzVcJJ9nZQMou2l6S0BDLFRDHqRW8xTQH6ZQzFtXLzAzG2LQwRD8JUMOngO +sMbNGWVn1ZXw6rYNNECdRORXEgdMl2hnT9y4hUFKbK7nGnv2cUQjtotK2Z3m68WY +AOwTROZgFr7283+z0v0iIPxLj6Y8ifrw1mTDsShGFRDF2Ia3GFd2BtP53mhGy8ar +9RKzKpAyelqt6l1o6ECbqwB3MfghUM4Or9zlctzOuqBxWB4E4DJ5OdwKDwJFUxL/ +9DmmbUzMJ6H7nhaTDgfQ1bihGCJ1EApTiA4S7A/7Ig18n4gNi9BzroL4790Wnh4w +A0hX5sPF2j609D3HeIdHGG+9Zl+u2xlwG0HRdjul3+5ddYypU5ierP9EZJiKUWtR +8KiJs9sHvZ4K05WKKn261+8uBhWYG9cvDo/hLtrXe2IjUciCO7woUqbBEIDG756W +yj2EPi10zBrrspGQcbEFGuyO+2kgAtPigpib4JYTkFXdt4kCRAQYAQIADwUCUaHr +LwIbAgUJAeEzgAEpCRDk6BMj55VO0MBdIAQZAQIABgUCUaHrLwAKCRAK+or5ywZS +K4umCACh/7AqZo7d9rCMJpdaOnk7I6FpNWHbW/J97ik7gqMCOYdIVC+JqCVcqKdy +tS/UGBEmpUPKxJF9ZXRq55poS9hJlMEUxX2G9DY1jwydzQIHhypF+UYydV1i88r0 +R19e8qR1PU3OMUDEbR03LVBSM6tqAJw6xKGS+VGL4Jck0SkB9F7WSOy8Q1MjkWOd +Nn6fFzyD7AFxRQuaVgc08g6LgjXkQ/4MSGDGD0L8SYbJsK70HO1fw12ccHomGyhy +WsgH2DLxJECzWOnShWpSc0g8ADV0Qokw2k9b+tNOCiH+8wxCj1JHslCGzLWDzvnH +vip4xfPebNa2Ehk795qfPUzc2nKK1QcH/jXogZm8E+0H/G0ys28PQf0HbpwgHMgM +aUyHZEBuzsWmk4n0AHF+qqA3W5kZ9F/wiVXGyNRq0RXU8+qaQsUC2oZpeYFbXZqM +SDTlyJnjpi7curmWC7cmrxgdFSHDvlHwx/1sl8k5QgKaCYuisj43rZXjfOvBG0mc +OqOgXjYCNaaaYyldVBxla0b+TbpwwHkaaewR0zKhTj2VM6CoyHsOUfMcKmboxRXJ +icxQ/xZ+XbogwKsK4edFodJqShsBYfosFV+62c7Fk875FbtbL3I4TbGph5x8vmZG +mZFuI2tG369Xx/HXQN7lcEbPKO8OGY/vNgWfbHrBHHpMgqTlMx/Q0ww= +=01Sl +-----END PGP PRIVATE KEY BLOCK----- diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 6bc9727..e57b07c 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -782,14 +782,42 @@ authentication.""" self.assertEqual(message, decrypted) - def test_decryption_multi_recipient(self): + def test_encryption_decryption_multi_recipient(self): """Test decryption of an encrypted string for multiple users""" - key = self.generate_key("Craig Gentry", "xorr.ox", - passphrase="craiggentry") - gentry = key.fingerprint - key = self.generate_key("Marten van Dijk", "xorr.ox", - passphrase="martenvandijk") - dijk = key.fingerprint + + alice = open(os.path.join(_files, 'test_key_1.pub')) + alice_pub = alice.read() + alice_public = self.gpg.import_keys(alice_pub) + res = alice_public.results[-1:][0] + alice_pfpr = str(res['fingerprint']) + alice.close() + + alice = open(os.path.join(_files, 'test_key_1.sec')) + alice_priv = alice.read() + alice_private = self.gpg.import_keys(alice_priv) + res = alice_private.results[-1:][0] + alice_sfpr = str(res['fingerprint']) + alice.close() + + bob = open(os.path.join(_files, 'test_key_2.pub')) + bob_pub = bob.read() + bob_public = self.gpg.import_keys(bob_pub) + res = bob_public.results[-1:][0] + bob_pfpr = str(res['fingerprint']) + bob.close() + + bob = open(os.path.join(_files, 'test_key_2.sec')) + bob_priv = bob.read() + bob_private = self.gpg.import_keys(bob_priv) + res = bob_public.results[-1:][0] + bob_sfpr = str(res['fingerprint']) + bob.close() + + log.debug("alice public fpr: %s" % alice_pfpr) + log.debug("alice public fpr: %s" % alice_sfpr) + log.debug("bob public fpr: %s" % bob_pfpr) + log.debug("bob public fpr: %s" % bob_sfpr) + message = """ In 2010 Riggio and Sicari presented a practical application of homomorphic encryption to a hybrid wireless sensor/mesh network. The system enables @@ -797,12 +825,16 @@ transparent multi-hop wireless backhauls that are able to perform statistical analysis of different kinds of data (temperature, humidity, etc.) coming from a WSN while ensuring both end-to-end encryption and hop-by-hop authentication.""" - encrypted = str(self.gpg.encrypt(message, [gentry, dijk])) + enc = self.gpg.encrypt(message, alice_pfpr, bob_pfpr) + encrypted = str(enc.data) + log.debug("encryption_decryption_multi_recipient() Ciphertext = %s" + % encrypted) + self.assertNotEqual(message, encrypted) - decrypted1 = self.gpg.decrypt(encrypted, passphrase="craiggentry") - self.assertEqual(message, str(decrypted1.data)) - decrypted2 = self.gpg.decrypt(encrypted, passphrase="martenvandijk") - self.assertEqual(message, str(decrypted2.data)) + dec_alice = self.gpg.decrypt(encrypted, passphrase="test") + self.assertEqual(message, str(dec_alice.data)) + dec_bob = self.gpg.decrypt(encrypted, passphrase="test") + self.assertEqual(message, str(dec_bob.data)) def test_symmetric_encryption_and_decryption(self): """Test symmetric encryption and decryption""" From fa57bc61581b193f2b264dc1274ece3d3810b976 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:30:05 +0000 Subject: [PATCH 386/397] Change unittest for symmetric encryption to include correct boolean options. --- tests/test_gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index e57b07c..ddd3b69 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -841,7 +841,7 @@ authentication.""" msg = """If you have something that you don't want anyone to know, maybe you shouldn't be doing it in the first place. - Eric Schmidt, CEO of Google""" - encrypted = str(self.gpg.encrypt(msg, None, passphrase='quiscustodiet', + encrypted = str(self.gpg.encrypt(msg, passphrase='quiscustodiet', symmetric=True, encrypt=False)) decrypted = self.gpg.decrypt(encrypted, passphrase='quiscustodiet') self.assertEqual(msg, str(decrypted.data)) From 87536c194ee8ab21f25f600083b0ed30b8fbdec2 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:32:52 +0000 Subject: [PATCH 387/397] Rewrite unittest for file encryption and decryption. --- tests/files/kat.sec | 138 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_gnupg.py | 50 ++++++++-------- 2 files changed, 164 insertions(+), 24 deletions(-) create mode 100644 tests/files/kat.sec diff --git a/tests/files/kat.sec b/tests/files/kat.sec new file mode 100644 index 0000000..b8c960e --- /dev/null +++ b/tests/files/kat.sec @@ -0,0 +1,138 @@ +## 1024R primary secret key, usage C, pasphrase 'katpics' +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=66aD +-----END PGP PRIVATE KEY BLOCK----- + +## 1024R subkey, usage ES, passphrase 'katpics' +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +TNnjCWTKcw== +=AVxa +-----END PGP PRIVATE KEY BLOCK----- diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index ddd3b69..aa7dcee 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -848,30 +848,32 @@ authentication.""" def test_file_encryption_and_decryption(self): """Test that encryption/decryption to/from file works.""" - key = self.generate_key("Andrew Able", "alpha.com", - passphrase="andrewable") - andrew = key.fingerprint - key = self.generate_key("Barbara Brown", "beta.com", - passphrase="barbarabrown") - barbara = key.fingerprint - enc_outf = file(os.path.join(self.gpg.homedir, 'to-b.gpg'), 'w+') - dec_outf = file(os.path.join(self.gpg.homedir, 'to-b.txt'), 'w+') - data = "Hello, world!" - data_file = _util._make_binary_stream(data, self.gpg.encoding) - edata = self.gpg.encrypt_file(data_file, barbara, - armor=False, - output=enc_outf) - ddata = self.gpg.decrypt_file(enc_outf, passphrase="barbarabrown", - output=dec_outf) - enc_outf.seek(0, 0) # can't use os.SEEK_SET in 2.4 - dec_outf.seek(0, 0) - enc_data = enc_outf.read() - dec_data = dec_outf.read() - data = data.encode(self.gpg.encoding) - if ddata != data: - log.debug("was: %r", data) - log.debug("new: %r", dec_data) - self.assertEqual(data, dec_data) + with open(os.path.join(_files, 'kat.sec')) as katsec: + self.gpg.import_keys(katsec.read()) + + kat = self.gpg.list_keys('kat')[0]['fingerprint'] + + enc_outf = os.path.join(self.gpg.homedir, 'to-b.gpg') + dec_outf = os.path.join(self.gpg.homedir, 'to-b.txt') + + message_file = os.path.join(_files, 'cypherpunk_manifesto') + with open(message_file) as msg: + data = msg.read() + ## GnuPG seems to ignore the output directive... + edata = self.gpg.encrypt(data, kat, output=enc_outf) + with open(enc_outf, 'w+') as enc: + enc.write(str(edata)) + + with open(enc_outf) as enc2: + fdata = enc2.read() + ddata = str(self.gpg.decrypt(fdata, passphrase="overalls")) + + data = data.encode(self.gpg.encoding) + if ddata != data: + log.debug("data was: %r" % data) + log.debug("new (from filehandle): %r" % fdata) + log.debug("new (from decryption): %r" % ddata) + self.assertEqual(data, ddata) suites = { 'parsers': set(['test_parsers_fix_unsafe', From 613f59eeff595d7e05d36f36d4038b838014617d Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:33:52 +0000 Subject: [PATCH 388/397] =?UTF-8?q?test=5Fdecryption=5Fmulti=5Frecipient?= =?UTF-8?q?=E2=86=92test=5Fencryption=5Fdecryption=5Fmulti=5Frecipient?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_gnupg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index aa7dcee..23fd57c 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -918,8 +918,8 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'crypt': set(['test_encryption', 'test_encryption_alt_encoding', 'test_encryption_multi_recipient', + 'test_encryption_decryption_multi_recipient', 'test_decryption', - 'test_decryption_multi_recipient', 'test_symmetric_encryption_and_decryption', 'test_file_encryption_and_decryption']), 'listkeys': set(['test_list_keys_after_generation']), From 73cfafbbdf80238001f446a4a9595588eb728461 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:35:06 +0000 Subject: [PATCH 389/397] Remove a pdb tracepoint now that all the unittests pass. :) --- tests/test_gnupg.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_gnupg.py b/tests/test_gnupg.py index 23fd57c..4435bd3 100644 --- a/tests/test_gnupg.py +++ b/tests/test_gnupg.py @@ -962,8 +962,6 @@ def main(args): prog = unittest.TestProgram prog.createTests = _createTests - import pdb - pdb.set_trace() program = prog(module=GPGTestCase, testRunner=runner, testLoader=loader, From 077773cb73bad30a84ef079dad44472184a5c813 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:38:41 +0000 Subject: [PATCH 390/397] Add line to .gitignore for keys generated during unittests. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 244111b..cf98e70 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,6 @@ local/* # Ignore gpg binary symlinks: gpg + +# Ignore keys generated during tests: +*generated-keys* From af5e67aa913d0a894fcadf7b7399fba4f598926f Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Mon, 27 May 2013 09:39:31 +0000 Subject: [PATCH 391/397] Add docs/DETAILS, from git://git.gnupg.org/gnupg.git branch 'STABLE-BRANCH-2-0' --- docs/DETAILS | 1225 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1225 insertions(+) create mode 100644 docs/DETAILS diff --git a/docs/DETAILS b/docs/DETAILS new file mode 100644 index 0000000..d5c5cea --- /dev/null +++ b/docs/DETAILS @@ -0,0 +1,1225 @@ +# doc/DETAILS -*- org -*- +#+TITLE: GnuPG Details +# Globally disable superscripts and subscripts: +#+OPTIONS: ^:{} +# + +# Note: This file uses org-mode; it should be easy to read as plain +# text but be aware of some markup peculiarities: Verbatim code is +# enclosed in #+begin-example, #+end-example blocks or marked by a +# colon as the first non-white-space character, words bracketed with +# equal signs indicate a monospace font, and the usual /italics/, +# *bold*, and _underline_ conventions are recognized. + +This is the DETAILS file for GnuPG which specifies some internals and +parts of the external API for GPG and GPGSM. + +* Format of the colon listings + The format is a based on colon separated record, each recods starts + with a tag string and extends to the end of the line. Here is an + example: +#+begin_example +$ gpg --with-colons --list-keys \ + --with-fingerprint --with-fingerprint wk@gnupg.org +pub:f:1024:17:6C7EE1B8621CC013:899817715:1055898235::m:::scESC: +fpr:::::::::ECAF7590EB3443B5C7CF3ACB6C7EE1B8621CC013: +uid:f::::::::Werner Koch : +uid:f::::::::Werner Koch : +sub:f:1536:16:06AD222CADF6A6E1:919537416:1036177416:::::e: +fpr:::::::::CF8BCC4B18DE08FCD8A1615906AD222CADF6A6E1: +sub:r:1536:20:5CE086B5B5A18FF4:899817788:1025961788:::::esc: +fpr:::::::::AB059359A3B81F410FCFF97F5CE086B5B5A18FF4: +#+end_example + +The double =--with-fingerprint= prints the fingerprint for the subkeys +too. Old versions of gpg used a lighly different format and required +the use of the option =--fixed-list-mode= to conform to format +described here. + +** Description of the fields +*** Field 1 - Type of record + + - pub :: Public key + - crt :: X.509 certificate + - crs :: X.509 certificate and private key available + - sub :: Subkey (secondary key) + - sec :: Secret key + - ssb :: Secret subkey (secondary key) + - uid :: User id (only field 10 is used). + - uat :: User attribute (same as user id except for field 10). + - sig :: Signature + - rev :: Revocation signature + - fpr :: Fingerprint (fingerprint is in field 10) + - pkd :: Public key data [*] + - grp :: Keygrip + - rvk :: Revocation key + - tru :: Trust database information [*] + - spk :: Signature subpacket [*] + - cfg :: Configuration data [*] + + Records marked with an asterisk are described at [[*Special%20field%20formats][*Special fields]]. + +*** Field 2 - Validity + + This is a letter describing the computed validity of a key. + Currently this is a single letter, but be prepared that additional + information may follow in some future versions. Note that GnuPG < + 2.1 does not set this field for secret key listings. + + - o :: Unknown (this key is new to the system) + - i :: The key is invalid (e.g. due to a missing self-signature) + - d :: The key has been disabled + (deprecated - use the 'D' in field 12 instead) + - r :: The key has been revoked + - e :: The key has expired + - - :: Unknown validity (i.e. no value assigned) + - q :: Undefined validity. '-' and 'q' may safely be treated as + the same value for most purposes + - n :: The key is not valid + - m :: The key is marginal valid. + - f :: The key is fully valid + - u :: The key is ultimately valid. This often means that the + secret key is available, but any key may be marked as + ultimately valid. + - w :: The key has a well known private part. + - s :: The key has special validity. This means that it might be + self-signed and expected to be used in the STEED sytem. + + If the validity information is given for a UID or UAT record, it + describes the validity calculated based on this user ID. If given + for a key record it describes the validity taken from the best + rated user ID. + + For X.509 certificates a 'u' is used for a trusted root + certificate (i.e. for the trust anchor) and an 'f' for all other + valid certificates. + +*** Field 3 - Key length + + The length of key in bits. + +*** Field 4 - Public key algorithm + + The values here are those from the OpenPGP specs or if they are + greather than 255 the algorithm ids as used by Libgcrypt. + +*** Field 5 - KeyID + + This is the 64 bit keyid as specified by OpenPGP and the last 64 + bit of the SHA-1 fingerprint of an X.509 certifciate. + +*** Field 6 - Creation date + + The creation date of the key is given in UTC. For UID and UAT + records, this is used for the self-signature date. Note that the + date is usally printed in seconds since epoch, however, we are + migrating to an ISO 8601 format (e.g. "19660205T091500"). This is + currently only relevant for X.509. A simple way to detect the new + format is to scan for the 'T'. Note that old versions of gpg + without using the =--fixed-list-mode= option used a "yyyy-mm-tt" + format. + +*** Field 7 - Expiration date + + Key or UID/UAT expiration date or empty if it does not expire. + +*** Field 8 - Certificate S/N, UID hash, trust signature info + + Used for serial number in crt records. For UID and UAT records, + this is a hash of the user ID contents used to represent that + exact user ID. For trust signatures, this is the trust depth + seperated by the trust value by a space. + +*** Field 9 - Ownertrust + + This is only used on primary keys. This is a single letter, but + be prepared that additional information may follow in future + versions. For trust signatures with a regular expression, this is + the regular expression value, quoted as in field 10. + +*** Field 10 - User-ID + The value is quoted like a C string to avoid control characters + (the colon is quoted =\x3a=). For a "pub" record this field is + not used on --fixed-list-mode. A UAT record puts the attribute + subpacket count here, a space, and then the total attribute + subpacket size. In gpgsm the issuer name comes here. A FPR + record stores the fingerprint here. The fingerprint of a + revocation key is stored here. +*** Field 11 - Signature class + + Signature class as per RFC-4880. This is a 2 digit hexnumber + followed by either the letter 'x' for an exportable signature or + the letter 'l' for a local-only signature. The class byte of an + revocation key is also given here, 'x' and 'l' is used the same + way. This field if not used for X.509. + +*** Field 12 - Key capabilities + + The defined capabilities are: + + - e :: Encrypt + - s :: Sign + - c :: Certify + - a :: Authentication + - ? :: Unknown capability + + A key may have any combination of them in any order. In addition + to these letters, the primary key has uppercase versions of the + letters to denote the _usable_ capabilities of the entire key, and + a potential letter 'D' to indicate a disabled key. + +*** Field 13 - Issuer certificate fingerprint or other info + + Used in FPR records for S/MIME keys to store the fingerprint of + the issuer certificate. This is useful to build the certificate + path based on certificates stored in the local key database it is + only filled if the issuer certificate is available. The root has + been reached if this is the same string as the fingerprint. The + advantage of using this value is that it is guaranteed to have + been been build by the same lookup algorithm as gpgsm uses. + + For "uid" records this field lists the preferences in the same way + gpg's --edit-key menu does. + + For "sig" records, this is the fingerprint of the key that issued + the signature. Note that this is only filled in if the signature + verified correctly. Note also that for various technical reasons, + this fingerprint is only available if --no-sig-cache is used. + +*** Field 14 - Flag field + + Flag field used in the --edit menu output + +*** Field 15 - S/N of a token + + Used in sec/sbb to print the serial number of a token (internal + protect mode 1002) or a '#' if that key is a simple stub (internal + protect mode 1001) + +*** Field 16 - Hash algorithm + + For sig records, this is the used hash algorithm. For example: + 2 = SHA-1, 8 = SHA-256. + +** Special fields + +*** PKD - Public key data + + If field 1 has the tag "pkd", a listing looks like this: +#+begin_example +pkd:0:1024:B665B1435F4C2 .... FF26ABB: + ! ! !-- the value + ! !------ for information number of bits in the value + !--------- index (eg. DSA goes from 0 to 3: p,q,g,y) +#+end_example + +*** TRU - Trust database information + Example for a "tru" trust base record: +#+begin_example + tru:o:0:1166697654:1:3:1:5 +#+end_example + + - Field 2 :: Reason for staleness of trust. If this field is + empty, then the trustdb is not stale. This field may + have multiple flags in it: + + - o :: Trustdb is old + - t :: Trustdb was built with a different trust model + than the one we are using now. + + - Field 3 :: Trust model + + - 0 :: Classic trust model, as used in PGP 2.x. + - 1 :: PGP trust model, as used in PGP 6 and later. + This is the same as the classic trust model, + except for the addition of trust signatures. + + GnuPG before version 1.4 used the classic trust model + by default. GnuPG 1.4 and later uses the PGP trust + model by default. + + - Field 4 :: Date trustdb was created in seconds since Epoch. + - Field 5 :: Date trustdb will expire in seconds since Epoch. + - Field 6 :: Number of marginally trusted users to introduce a new + key signer (gpg's option --marginals-needed). + - Field 7 :: Number of completely trusted users to introduce a new + key signer. (gpg's option --completes-needed) + + - Field 8 :: Maximum depth of a certification chain. (gpg's option + --max-cert-depth) + +*** SPK - Signature subpacket records + + - Field 2 :: Subpacket number as per RFC-4880 and later. + - Field 3 :: Flags in hex. Currently the only two bits assigned + are 1, to indicate that the subpacket came from the + hashed part of the signature, and 2, to indicate the + subpacket was marked critical. + - Field 4 :: Length of the subpacket. Note that this is the + length of the subpacket, and not the length of field + 5 below. Due to the need for %-encoding, the length + of field 5 may be up to 3x this value. + - Field 5 :: The subpacket data. Printable ASCII is shown as + ASCII, but other values are rendered as %XX where XX + is the hex value for the byte. + +*** CFG - Configuration data + + --list-config outputs information about the GnuPG configuration + for the benefit of frontends or other programs that call GnuPG. + There are several list-config items, all colon delimited like the + rest of the --with-colons output. The first field is always "cfg" + to indicate configuration information. The second field is one of + (with examples): + + - version :: The third field contains the version of GnuPG. + + : cfg:version:1.3.5 + + - pubkey :: The third field contains the public key algorithms + this version of GnuPG supports, separated by + semicolons. The algorithm numbers are as specified in + RFC-4880. Note that in contrast to the --status-fd + interface these are _not_ the Libgcrypt identifiers. + + : cfg:pubkey:1;2;3;16;17 + + - cipher :: The third field contains the symmetric ciphers this + version of GnuPG supports, separated by semicolons. + The cipher numbers are as specified in RFC-4880. + + : cfg:cipher:2;3;4;7;8;9;10 + + - digest :: The third field contains the digest (hash) algorithms + this version of GnuPG supports, separated by + semicolons. The digest numbers are as specified in + RFC-4880. + + : cfg:digest:1;2;3;8;9;10 + + - compress :: The third field contains the compression algorithms + this version of GnuPG supports, separated by + semicolons. The algorithm numbers are as specified + in RFC-4880. + + : cfg:compress:0;1;2;3 + + - group :: The third field contains the name of the group, and the + fourth field contains the values that the group expands + to, separated by semicolons. + + For example, a group of: + : group mynames = paige 0x12345678 joe patti + would result in: + : cfg:group:mynames:patti;joe;0x12345678;paige + + +* Format of the --status-fd output + + Every line is prefixed with "[GNUPG:] ", followed by a keyword with + the type of the status line and some arguments depending on the type + (maybe none); an application should always be prepared to see more + arguments in future versions. + +** General status codes +*** NEWSIG + May be issued right before a signature verification starts. This + is useful to define a context for parsing ERROR status messages. + No arguments are currently defined. + +*** GOODSIG + The signature with the keyid is good. For each signature only one + of the codes GOODSIG, BADSIG, EXPSIG, EXPKEYSIG, REVKEYSIG or + ERRSIG will be emitted. In the past they were used as a marker + for a new signature; new code should use the NEWSIG status + instead. The username is the primary one encoded in UTF-8 and %XX + escaped. The fingerprint may be used instead of the long keyid if + it is available. This is the case with CMS and might eventually + also be available for OpenPGP. + +*** EXPSIG + The signature with the keyid is good, but the signature is + expired. The username is the primary one encoded in UTF-8 and %XX + escaped. The fingerprint may be used instead of the long keyid if + it is available. This is the case with CMS and might eventually + also be available for OpenPGP. + +*** EXPKEYSIG + The signature with the keyid is good, but the signature was made + by an expired key. The username is the primary one encoded in + UTF-8 and %XX escaped. The fingerprint may be used instead of the + long keyid if it is available. This is the case with CMS and + might eventually also be available for OpenPGP. + +*** REVKEYSIG + The signature with the keyid is good, but the signature was made + by a revoked key. The username is the primary one encoded in UTF-8 + and %XX escaped. The fingerprint may be used instead of the long + keyid if it is available. This is the case with CMS and might + eventually also beƱ available for OpenPGP. + +*** BADSIG + The signature with the keyid has not been verified okay. The + username is the primary one encoded in UTF-8 and %XX escaped. The + fingerprint may be used instead of the long keyid if it is + available. This is the case with CMS and might eventually also be + available for OpenPGP. + +*** ERRSIG