diff options
Diffstat (limited to 'lang')
209 files changed, 33940 insertions, 917 deletions
diff --git a/lang/Makefile.am b/lang/Makefile.am index fd3ce4e..1bf7331 100644 --- a/lang/Makefile.am +++ b/lang/Makefile.am @@ -18,6 +18,6 @@ # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA SUBDIRS = $(ENABLED_LANGUAGES) -DIST_SUBDIRS = cl cpp qt python +DIST_SUBDIRS = cl cpp qt python js EXTRA_DIST = README diff --git a/lang/Makefile.in b/lang/Makefile.in index 9ca2ef6..8c89676 100644 --- a/lang/Makefile.in +++ b/lang/Makefile.in @@ -114,7 +114,7 @@ am__aclocal_m4_deps = $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ $(ACLOCAL_M4) mkinstalldirs = $(SHELL) $(top_srcdir)/build-aux/mkinstalldirs -CONFIG_HEADER = $(top_builddir)/config.h +CONFIG_HEADER = $(top_builddir)/conf/config.h CONFIG_CLEAN_FILES = CONFIG_CLEAN_VPATH_FILES = AM_V_P = $(am__v_P_@AM_V@) @@ -392,7 +392,7 @@ top_build_prefix = @top_build_prefix@ top_builddir = @top_builddir@ top_srcdir = @top_srcdir@ SUBDIRS = $(ENABLED_LANGUAGES) -DIST_SUBDIRS = cl cpp qt python +DIST_SUBDIRS = cl cpp qt python js EXTRA_DIST = README all: all-recursive diff --git a/lang/README b/lang/README index ee99f0f..afd7b08 100644 --- a/lang/README +++ b/lang/README @@ -13,4 +13,4 @@ cl Common Lisp cpp C++ qt Qt-Framework API python Python 2 and 3 (module name: gpg) -javascript Native messaging client for the gpgme-json server. +js Native messaging client for the gpgme-json server. diff --git a/lang/cl/Makefile.in b/lang/cl/Makefile.in index 8cb02bd..5c492d7 100644 --- a/lang/cl/Makefile.in +++ b/lang/cl/Makefile.in @@ -117,7 +117,7 @@ am__aclocal_m4_deps = $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ $(ACLOCAL_M4) mkinstalldirs = $(SHELL) $(top_srcdir)/build-aux/mkinstalldirs -CONFIG_HEADER = $(top_builddir)/config.h +CONFIG_HEADER = $(top_builddir)/conf/config.h CONFIG_CLEAN_FILES = gpgme.asd CONFIG_CLEAN_VPATH_FILES = AM_V_P = $(am__v_P_@AM_V@) diff --git a/lang/cl/gpgme.asd b/lang/cl/gpgme.asd index 9c5c719..5f3e4a0 100644 --- a/lang/cl/gpgme.asd +++ b/lang/cl/gpgme.asd @@ -27,7 +27,7 @@ (defsystem gpgme :description "GnuPG Made Easy." :author "g10 Code GmbH" - :version "1.11.1" + :version "1.12.0" :licence "GPL" :depends-on ("cffi" "gpg-error") :components ((:file "gpgme-package") diff --git a/lang/cpp/Makefile.in b/lang/cpp/Makefile.in index 0527499..f276c3f 100644 --- a/lang/cpp/Makefile.in +++ b/lang/cpp/Makefile.in @@ -116,7 +116,7 @@ am__aclocal_m4_deps = $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ $(ACLOCAL_M4) mkinstalldirs = $(SHELL) $(top_srcdir)/build-aux/mkinstalldirs -CONFIG_HEADER = $(top_builddir)/config.h +CONFIG_HEADER = $(top_builddir)/conf/config.h CONFIG_CLEAN_FILES = CONFIG_CLEAN_VPATH_FILES = AM_V_P = $(am__v_P_@AM_V@) diff --git a/lang/cpp/src/Makefile.in b/lang/cpp/src/Makefile.in index 21b259d..5ab55cc 100644 --- a/lang/cpp/src/Makefile.in +++ b/lang/cpp/src/Makefile.in @@ -123,7 +123,7 @@ am__aclocal_m4_deps = $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ $(ACLOCAL_M4) mkinstalldirs = $(SHELL) $(top_srcdir)/build-aux/mkinstalldirs -CONFIG_HEADER = $(top_builddir)/config.h +CONFIG_HEADER = $(top_builddir)/conf/config.h CONFIG_CLEAN_FILES = GpgmeppConfig-w32.cmake.in GpgmeppConfig.cmake.in \ GpgmeppConfigVersion.cmake gpgmepp_version.h CONFIG_CLEAN_VPATH_FILES = @@ -195,7 +195,7 @@ AM_V_at = $(am__v_at_@AM_V@) am__v_at_ = $(am__v_at_@AM_DEFAULT_V@) am__v_at_0 = @ am__v_at_1 = -DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir) +DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)/conf depcomp = $(SHELL) $(top_srcdir)/build-aux/depcomp am__depfiles_maybe = depfiles am__mv = mv -f diff --git a/lang/cpp/src/context.cpp b/lang/cpp/src/context.cpp index 135e4d5..1e4e549 100644 --- a/lang/cpp/src/context.cpp +++ b/lang/cpp/src/context.cpp @@ -1028,6 +1028,9 @@ unsigned int to_auditlog_flags(unsigned int flags) if (flags & Context::AuditLogWithHelp) { result |= GPGME_AUDITLOG_WITH_HELP; } + if (flags & Context::DiagnosticAuditLog) { + result |= GPGME_AUDITLOG_DIAG; + } return result; } @@ -1436,6 +1439,23 @@ Error Context::createKey (const char *userid, flags)); } +KeyGenerationResult Context::createKeyEx (const char *userid, + const char *algo, + unsigned long reserved, + unsigned long expires, + const Key &certkey, + unsigned int flags) +{ + d->lasterr = gpgme_op_createkey(d->ctx, + userid, + algo, + reserved, + expires, + certkey.impl(), + flags); + return KeyGenerationResult(d->ctx, Error(d->lasterr)); +} + Error Context::addUid(const Key &k, const char *userid) { return Error(d->lasterr = gpgme_op_adduid(d->ctx, @@ -1478,6 +1498,16 @@ Error Context::startCreateSubkey(const Key &k, const char *algo, k.impl(), algo, reserved, expires, flags)); } +Error Context::setFlag(const char *name, const char *value) +{ + return Error(d->lasterr = gpgme_set_ctx_flag(d->ctx, name, value)); +} + +const char *Context::getFlag(const char *name) const +{ + return gpgme_get_ctx_flag(d->ctx, name); +} + // Engine Spawn stuff Error Context::spawn(const char *file, const char *argv[], Data &input, Data &output, Data &err, diff --git a/lang/cpp/src/context.h b/lang/cpp/src/context.h index aff8e49..6e27daa 100644 --- a/lang/cpp/src/context.h +++ b/lang/cpp/src/context.h @@ -86,6 +86,9 @@ public: void setOffline(bool useOfflineMode); bool offline() const; + const char *getFlag(const char *name) const; + Error setFlag(const char *name, const char *value); + enum CertificateInclusion { DefaultCertificates = -256, AllCertificatesExceptRoot = -2, @@ -231,6 +234,14 @@ public: const Key &certkey, unsigned int flags); + // Same as create key but returning a result + GpgME::KeyGenerationResult createKeyEx (const char *userid, + const char *algo, + unsigned long reserved, + unsigned long expires, + const Key &certkey, + unsigned int flags); + Error addUid(const Key &key, const char *userid); Error startAddUid(const Key &key, const char *userid); @@ -390,7 +401,9 @@ public: // // enum AuditLogFlags { + DefaultAuditLog = 0, HtmlAuditLog = 1, + DiagnosticAuditLog = 2, AuditLogWithHelp = 128 }; GpgME::Error startGetAuditLog(Data &output, unsigned int flags = 0); @@ -453,6 +466,7 @@ public: { return d; } + private: // Helper functions that need to be context because they rely // on the "Friendlyness" of context to access the gpgme types. diff --git a/lang/cpp/src/data.cpp b/lang/cpp/src/data.cpp index 52b8da2..2782aa7 100644 --- a/lang/cpp/src/data.cpp +++ b/lang/cpp/src/data.cpp @@ -232,6 +232,11 @@ off_t GpgME::Data::seek(off_t offset, int whence) return gpgme_data_seek(d->data, offset, whence); } +GpgME::Error GpgME::Data::rewind() +{ + return Error(gpgme_data_rewind(d->data)); +} + std::vector<GpgME::Key> GpgME::Data::toKeys(Protocol proto) const { std::vector<GpgME::Key> ret; diff --git a/lang/cpp/src/data.h b/lang/cpp/src/data.h index 446f6fa..df8607e 100644 --- a/lang/cpp/src/data.h +++ b/lang/cpp/src/data.h @@ -110,6 +110,9 @@ public: ssize_t write(const void *buffer, size_t length); off_t seek(off_t offset, int whence); + /* Convenience function to do a seek (0, SEEK_SET). */ + Error rewind(); + /** Try to parse the data to a key object using the * Protocol proto. Returns an empty list on error.*/ std::vector<Key> toKeys(const Protocol proto = Protocol::OpenPGP) const; diff --git a/lang/cpp/src/decryptionresult.cpp b/lang/cpp/src/decryptionresult.cpp index 1e815cb..ea0a8a5 100644 --- a/lang/cpp/src/decryptionresult.cpp +++ b/lang/cpp/src/decryptionresult.cpp @@ -51,6 +51,9 @@ public: if (res.file_name) { res.file_name = strdup(res.file_name); } + if (res.symkey_algo) { + res.symkey_algo = strdup(res.symkey_algo); + } //FIXME: copying gpgme_recipient_t objects invalidates the keyid member, //thus we use _keyid for now (internal API) for (gpgme_recipient_t r = res.recipients ; r ; r = r->next) { @@ -68,6 +71,10 @@ public: std::free(res.file_name); } res.file_name = 0; + if (res.symkey_algo) { + std::free(res.symkey_algo); + } + res.symkey_algo = 0; } _gpgme_op_decrypt_result res; @@ -155,6 +162,21 @@ std::vector<GpgME::DecryptionResult::Recipient> GpgME::DecryptionResult::recipie return result; } +const char *GpgME::DecryptionResult::sessionKey() const +{ + return d ? d->res.session_key : nullptr; +} + +const char *GpgME::DecryptionResult::symkeyAlgo() const +{ + return d ? d->res.symkey_algo : nullptr; +} + +bool GpgME::DecryptionResult::isLegacyCipherNoMDC() const +{ + return d && d->res.legacy_cipher_nomdc; +} + class GpgME::DecryptionResult::Recipient::Private : public _gpgme_recipient { public: @@ -231,6 +253,8 @@ std::ostream &GpgME::operator<<(std::ostream &os, const DecryptionResult &result << "\n unsupportedAlgorithm: " << protect(result.unsupportedAlgorithm()) << "\n isWrongKeyUsage: " << result.isWrongKeyUsage() << "\n isDeVs " << result.isDeVs() + << "\n legacyCipherNoMDC " << result.isLegacyCipherNoMDC() + << "\n symkeyAlgo: " << protect(result.symkeyAlgo()) << "\n recipients:\n"; const std::vector<DecryptionResult::Recipient> recipients = result.recipients(); std::copy(recipients.begin(), recipients.end(), diff --git a/lang/cpp/src/decryptionresult.h b/lang/cpp/src/decryptionresult.h index 57705b4..e4d542d 100644 --- a/lang/cpp/src/decryptionresult.h +++ b/lang/cpp/src/decryptionresult.h @@ -77,12 +77,18 @@ public: const char *fileName() const; + const char *sessionKey() const; + + const char *symkeyAlgo() const; + class Recipient; unsigned int numRecipients() const; Recipient recipient(unsigned int idx) const; std::vector<Recipient> recipients() const; + bool isLegacyCipherNoMDC() const; + private: class Private; void init(gpgme_ctx_t ctx); diff --git a/lang/cpp/src/gpggencardkeyinteractor.cpp b/lang/cpp/src/gpggencardkeyinteractor.cpp index 6f42e47..0ed6781 100644 --- a/lang/cpp/src/gpggencardkeyinteractor.cpp +++ b/lang/cpp/src/gpggencardkeyinteractor.cpp @@ -36,12 +36,11 @@ using namespace GpgME; class GpgGenCardKeyInteractor::Private { public: - Private() : keysize(2048), backup(false) + Private() : keysize("2048"), backup(false) { } - std::string name, email, backupFileName, expiry, serial; - int keysize; + std::string name, email, backupFileName, expiry, serial, keysize; bool backup; }; @@ -70,7 +69,7 @@ void GpgGenCardKeyInteractor::setDoBackup(bool value) void GpgGenCardKeyInteractor::setKeySize(int value) { - d->keysize = value; + d->keysize = std::to_string(value); } void GpgGenCardKeyInteractor::setExpiry(const std::string &timeStr) @@ -132,7 +131,7 @@ const char *GpgGenCardKeyInteractor::action(Error &err) const case SIZE: case SIZE2: case SIZE3: - return std::to_string(d->keysize).c_str(); + return d->keysize.c_str(); case COMMENT: return ""; case SAVE: diff --git a/lang/cpp/src/key.cpp b/lang/cpp/src/key.cpp index 034286f..8fc266f 100644 --- a/lang/cpp/src/key.cpp +++ b/lang/cpp/src/key.cpp @@ -347,6 +347,9 @@ const Key &Key::mergeWith(const Key &other) void Key::update() { + if (isNull() || !primaryFingerprint()) { + return; + } auto ctx = Context::createForProtocol(protocol()); if (!ctx) { return; @@ -1042,6 +1045,8 @@ std::ostream &operator<<(std::ostream &os, const UserID &uid) << "\n revoked: " << uid.isRevoked() << "\n invalid: " << uid.isInvalid() << "\n numsigs: " << uid.numSignatures() + << "\n origin: " << uid.origin() + << "\n updated: " << uid.lastUpdate() << "\n tofuinfo:\n" << uid.tofuInfo(); } return os << ')'; @@ -1060,6 +1065,8 @@ std::ostream &operator<<(std::ostream &os, const Key &key) << "\n canEncrypt: " << key.canEncrypt() << "\n canCertify: " << key.canCertify() << "\n canAuth: " << key.canAuthenticate() + << "\n origin: " << key.origin() + << "\n updated: " << key.lastUpdate() << "\n uids:\n"; const std::vector<UserID> uids = key.userIDs(); std::copy(uids.begin(), uids.end(), diff --git a/lang/cpp/src/verificationresult.cpp b/lang/cpp/src/verificationresult.cpp index 2c42d07..fa8237a 100644 --- a/lang/cpp/src/verificationresult.cpp +++ b/lang/cpp/src/verificationresult.cpp @@ -406,7 +406,7 @@ GpgME::Key GpgME::Signature::key(bool search, bool update) const } GpgME::Key ret = key(); - if (ret.isNull() && search) { + if (ret.isNull() && search && fingerprint ()) { auto ctx = Context::createForProtocol (d->proto); if (ctx) { ctx->setKeyListMode(KeyListMode::Local | diff --git a/lang/js/.eslintrc.json b/lang/js/.eslintrc.json new file mode 100644 index 0000000..dc3be2e --- /dev/null +++ b/lang/js/.eslintrc.json @@ -0,0 +1,49 @@ +{ + "env": { + "browser": true, + "es6": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "indent": [ + "warn", + 4 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ], + "no-var": [ + "warn" + ], + "max-len": 1, + "default-case": 2, + "no-invalid-this": 2, + "no-lone-blocks": 1, + "no-self-compare": 2, + "radix": 2, + "no-use-before-define": ["error", { + "functions": false, + "classes": false, + "variables": true + }], + "no-useless-constructor": 1, + "space-before-function-paren": ["error", "always"], + "keyword-spacing": 2, + "spaced-comment": 1, + "space-unary-ops": 2, + "object-curly-spacing": ["error", "always"], + "array-bracket-spacing": ["error", "never"] + } +}
\ No newline at end of file diff --git a/lang/js/BrowserTestExtension/Makefile.am b/lang/js/BrowserTestExtension/Makefile.am new file mode 100644 index 0000000..8f0a4f9 --- /dev/null +++ b/lang/js/BrowserTestExtension/Makefile.am @@ -0,0 +1,45 @@ +# Makefile.am for gpgme.js. +# Copyright (C) 2018 Intevation GmbH +# +# This file is part of GPGME. +# +# gpgme.js is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# gpgme.js 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA +# 02111-1307, USA + +EXTRA_DIST = browsertest.html \ + index.html \ + longTests.html \ + Makefile.am \ + manifest.json \ + popup.html \ + popup.js \ + runbrowsertest.js \ + rununittests.js \ + setup_testing.js \ + testicon.png \ + testkey2.pub \ + testkey.pub \ + testkey.sec \ + tests/decryptTest.js \ + tests/encryptDecryptTest.js \ + tests/encryptTest.js \ + tests/inputvalues.js \ + tests/KeyImportExport.js \ + tests/KeyInfos.js \ + tests/longRunningTests.js \ + tests/signTest.js \ + tests/startup.js \ + tests/verifyTest.js \ + unittests.html diff --git a/lang/js/BrowserTestExtension/Makefile.in b/lang/js/BrowserTestExtension/Makefile.in new file mode 100644 index 0000000..842ca1a --- /dev/null +++ b/lang/js/BrowserTestExtension/Makefile.in @@ -0,0 +1,555 @@ +# Makefile.in generated by automake 1.14.1 from Makefile.am. +# @configure_input@ + +# Copyright (C) 1994-2013 Free Software Foundation, Inc. + +# This Makefile.in is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. + +@SET_MAKE@ + +# Makefile.am for gpgme.js. +# Copyright (C) 2018 Intevation GmbH +# +# This file is part of GPGME. +# +# gpgme.js is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# gpgme.js 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA +# 02111-1307, USA +VPATH = @srcdir@ +am__is_gnu_make = test -n '$(MAKEFILE_LIST)' && test -n '$(MAKELEVEL)' +am__make_running_with_option = \ + case $${target_option-} in \ + ?) ;; \ + *) echo "am__make_running_with_option: internal error: invalid" \ + "target option '$${target_option-}' specified" >&2; \ + exit 1;; \ + esac; \ + has_opt=no; \ + sane_makeflags=$$MAKEFLAGS; \ + if $(am__is_gnu_make); then \ + sane_makeflags=$$MFLAGS; \ + else \ + case $$MAKEFLAGS in \ + *\\[\ \ ]*) \ + bs=\\; \ + sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \ + | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \ + esac; \ + fi; \ + skip_next=no; \ + strip_trailopt () \ + { \ + flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \ + }; \ + for flg in $$sane_makeflags; do \ + test $$skip_next = yes && { skip_next=no; continue; }; \ + case $$flg in \ + *=*|--*) continue;; \ + -*I) strip_trailopt 'I'; skip_next=yes;; \ + -*I?*) strip_trailopt 'I';; \ + -*O) strip_trailopt 'O'; skip_next=yes;; \ + -*O?*) strip_trailopt 'O';; \ + -*l) strip_trailopt 'l'; skip_next=yes;; \ + -*l?*) strip_trailopt 'l';; \ + -[dEDm]) skip_next=yes;; \ + -[JT]) skip_next=yes;; \ + esac; \ + case $$flg in \ + *$$target_option*) has_opt=yes; break;; \ + esac; \ + done; \ + test $$has_opt = yes +am__make_dryrun = (target_option=n; $(am__make_running_with_option)) +am__make_keepgoing = (target_option=k; $(am__make_running_with_option)) +pkgdatadir = $(datadir)/@PACKAGE@ +pkgincludedir = $(includedir)/@PACKAGE@ +pkglibdir = $(libdir)/@PACKAGE@ +pkglibexecdir = $(libexecdir)/@PACKAGE@ +am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd +install_sh_DATA = $(install_sh) -c -m 644 +install_sh_PROGRAM = $(install_sh) -c +install_sh_SCRIPT = $(install_sh) -c +INSTALL_HEADER = $(INSTALL_DATA) +transform = $(program_transform_name) +NORMAL_INSTALL = : +PRE_INSTALL = : +POST_INSTALL = : +NORMAL_UNINSTALL = : +PRE_UNINSTALL = : +POST_UNINSTALL = : +build_triplet = @build@ +host_triplet = @host@ +subdir = lang/js/BrowserTestExtension +DIST_COMMON = $(srcdir)/Makefile.in $(srcdir)/Makefile.am \ + $(top_srcdir)/build-aux/mkinstalldirs +ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 +am__aclocal_m4_deps = $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ + $(top_srcdir)/m4/ax_pkg_swig.m4 \ + $(top_srcdir)/m4/ax_python_devel.m4 \ + $(top_srcdir)/m4/glib-2.0.m4 $(top_srcdir)/m4/glibc21.m4 \ + $(top_srcdir)/m4/gnupg-ttyname.m4 \ + $(top_srcdir)/m4/gpg-error.m4 $(top_srcdir)/m4/libassuan.m4 \ + $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/ltoptions.m4 \ + $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \ + $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/pkg.m4 \ + $(top_srcdir)/m4/python.m4 $(top_srcdir)/m4/qt.m4 \ + $(top_srcdir)/acinclude.m4 $(top_srcdir)/configure.ac +am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ + $(ACLOCAL_M4) +mkinstalldirs = $(SHELL) $(top_srcdir)/build-aux/mkinstalldirs +CONFIG_HEADER = $(top_builddir)/conf/config.h +CONFIG_CLEAN_FILES = +CONFIG_CLEAN_VPATH_FILES = +AM_V_P = $(am__v_P_@AM_V@) +am__v_P_ = $(am__v_P_@AM_DEFAULT_V@) +am__v_P_0 = false +am__v_P_1 = : +AM_V_GEN = $(am__v_GEN_@AM_V@) +am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@) +am__v_GEN_0 = @echo " GEN " $@; +am__v_GEN_1 = +AM_V_at = $(am__v_at_@AM_V@) +am__v_at_ = $(am__v_at_@AM_DEFAULT_V@) +am__v_at_0 = @ +am__v_at_1 = +SOURCES = +DIST_SOURCES = +am__can_run_installinfo = \ + case $$AM_UPDATE_INFO_DIR in \ + n|no|NO) false;; \ + *) (install-info --version) >/dev/null 2>&1;; \ + esac +am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP) +DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +ACLOCAL = @ACLOCAL@ +AMTAR = @AMTAR@ +AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@ +AR = @AR@ +AS = @AS@ +AUTOCONF = @AUTOCONF@ +AUTOHEADER = @AUTOHEADER@ +AUTOMAKE = @AUTOMAKE@ +AWK = @AWK@ +BUILD_FILEVERSION = @BUILD_FILEVERSION@ +BUILD_REVISION = @BUILD_REVISION@ +BUILD_TIMESTAMP = @BUILD_TIMESTAMP@ +CC = @CC@ +CCDEPMODE = @CCDEPMODE@ +CC_FOR_BUILD = @CC_FOR_BUILD@ +CFLAGS = @CFLAGS@ +CPP = @CPP@ +CPPFLAGS = @CPPFLAGS@ +CXX = @CXX@ +CXXCPP = @CXXCPP@ +CXXDEPMODE = @CXXDEPMODE@ +CXXFLAGS = @CXXFLAGS@ +CYGPATH_W = @CYGPATH_W@ +DEFS = @DEFS@ +DEPDIR = @DEPDIR@ +DLLTOOL = @DLLTOOL@ +DOXYGEN = @DOXYGEN@ +DSYMUTIL = @DSYMUTIL@ +DUMPBIN = @DUMPBIN@ +ECHO_C = @ECHO_C@ +ECHO_N = @ECHO_N@ +ECHO_T = @ECHO_T@ +EGREP = @EGREP@ +ENABLED_LANGUAGES = @ENABLED_LANGUAGES@ +EXEEXT = @EXEEXT@ +FGREP = @FGREP@ +GITLOG_TO_CHANGELOG = @GITLOG_TO_CHANGELOG@ +GLIBC21 = @GLIBC21@ +GLIB_CFLAGS = @GLIB_CFLAGS@ +GLIB_GENMARSHAL = @GLIB_GENMARSHAL@ +GLIB_LIBS = @GLIB_LIBS@ +GLIB_MKENUMS = @GLIB_MKENUMS@ +GOBJECT_QUERY = @GOBJECT_QUERY@ +GPGME_CONFIG_API_VERSION = @GPGME_CONFIG_API_VERSION@ +GPGME_CONFIG_AVAIL_LANG = @GPGME_CONFIG_AVAIL_LANG@ +GPGME_CONFIG_CFLAGS = @GPGME_CONFIG_CFLAGS@ +GPGME_CONFIG_HOST = @GPGME_CONFIG_HOST@ +GPGME_CONFIG_LIBS = @GPGME_CONFIG_LIBS@ +GPGME_QTTEST_CFLAGS = @GPGME_QTTEST_CFLAGS@ +GPGME_QTTEST_LIBS = @GPGME_QTTEST_LIBS@ +GPGME_QT_CFLAGS = @GPGME_QT_CFLAGS@ +GPGME_QT_LIBS = @GPGME_QT_LIBS@ +GPG_ERROR_CFLAGS = @GPG_ERROR_CFLAGS@ +GPG_ERROR_CONFIG = @GPG_ERROR_CONFIG@ +GPG_ERROR_LIBS = @GPG_ERROR_LIBS@ +GPG_ERROR_MT_CFLAGS = @GPG_ERROR_MT_CFLAGS@ +GPG_ERROR_MT_LIBS = @GPG_ERROR_MT_LIBS@ +GRAPHVIZ = @GRAPHVIZ@ +GREP = @GREP@ +HAVE_CXX11 = @HAVE_CXX11@ +HAVE_DOT = @HAVE_DOT@ +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ +INSTALL_PROGRAM = @INSTALL_PROGRAM@ +INSTALL_SCRIPT = @INSTALL_SCRIPT@ +INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@ +LD = @LD@ +LDFLAGS = @LDFLAGS@ +LIBASSUAN_CFLAGS = @LIBASSUAN_CFLAGS@ +LIBASSUAN_CONFIG = @LIBASSUAN_CONFIG@ +LIBASSUAN_LIBS = @LIBASSUAN_LIBS@ +LIBGPGMEPP_LT_AGE = @LIBGPGMEPP_LT_AGE@ +LIBGPGMEPP_LT_CURRENT = @LIBGPGMEPP_LT_CURRENT@ +LIBGPGMEPP_LT_REVISION = @LIBGPGMEPP_LT_REVISION@ +LIBGPGME_LT_AGE = @LIBGPGME_LT_AGE@ +LIBGPGME_LT_CURRENT = @LIBGPGME_LT_CURRENT@ +LIBGPGME_LT_REVISION = @LIBGPGME_LT_REVISION@ +LIBOBJS = @LIBOBJS@ +LIBQGPGME_LT_AGE = @LIBQGPGME_LT_AGE@ +LIBQGPGME_LT_CURRENT = @LIBQGPGME_LT_CURRENT@ +LIBQGPGME_LT_REVISION = @LIBQGPGME_LT_REVISION@ +LIBS = @LIBS@ +LIBTOOL = @LIBTOOL@ +LIPO = @LIPO@ +LN_S = @LN_S@ +LTLIBOBJS = @LTLIBOBJS@ +MAINT = @MAINT@ +MAKEINFO = @MAKEINFO@ +MANIFEST_TOOL = @MANIFEST_TOOL@ +MKDIR_P = @MKDIR_P@ +MOC = @MOC@ +MOC2 = @MOC2@ +NEED__FILE_OFFSET_BITS = @NEED__FILE_OFFSET_BITS@ +NM = @NM@ +NMEDIT = @NMEDIT@ +OBJDUMP = @OBJDUMP@ +OBJEXT = @OBJEXT@ +OTOOL = @OTOOL@ +OTOOL64 = @OTOOL64@ +PACKAGE = @PACKAGE@ +PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@ +PACKAGE_NAME = @PACKAGE_NAME@ +PACKAGE_STRING = @PACKAGE_STRING@ +PACKAGE_TARNAME = @PACKAGE_TARNAME@ +PACKAGE_URL = @PACKAGE_URL@ +PACKAGE_VERSION = @PACKAGE_VERSION@ +PATH_SEPARATOR = @PATH_SEPARATOR@ +PKG_CONFIG = @PKG_CONFIG@ +PYTHON = @PYTHON@ +PYTHONS = @PYTHONS@ +PYTHON_CPPFLAGS = @PYTHON_CPPFLAGS@ +PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@ +PYTHON_EXTRA_LDFLAGS = @PYTHON_EXTRA_LDFLAGS@ +PYTHON_EXTRA_LIBS = @PYTHON_EXTRA_LIBS@ +PYTHON_LDFLAGS = @PYTHON_LDFLAGS@ +PYTHON_PLATFORM = @PYTHON_PLATFORM@ +PYTHON_PREFIX = @PYTHON_PREFIX@ +PYTHON_SITE_PKG = @PYTHON_SITE_PKG@ +PYTHON_VERSION = @PYTHON_VERSION@ +QTCHOOSER = @QTCHOOSER@ +RANLIB = @RANLIB@ +RC = @RC@ +SED = @SED@ +SET_MAKE = @SET_MAKE@ +SHELL = @SHELL@ +STRIP = @STRIP@ +SWIG = @SWIG@ +SWIG_LIB = @SWIG_LIB@ +SYSROOT = @SYSROOT@ +VERSION = @VERSION@ +VERSION_MAJOR = @VERSION_MAJOR@ +VERSION_MICRO = @VERSION_MICRO@ +VERSION_MINOR = @VERSION_MINOR@ +VERSION_NUMBER = @VERSION_NUMBER@ +abs_builddir = @abs_builddir@ +abs_srcdir = @abs_srcdir@ +abs_top_builddir = @abs_top_builddir@ +abs_top_srcdir = @abs_top_srcdir@ +ac_ct_AR = @ac_ct_AR@ +ac_ct_CC = @ac_ct_CC@ +ac_ct_CXX = @ac_ct_CXX@ +ac_ct_DUMPBIN = @ac_ct_DUMPBIN@ +am__include = @am__include@ +am__leading_dot = @am__leading_dot@ +am__quote = @am__quote@ +am__tar = @am__tar@ +am__untar = @am__untar@ +bindir = @bindir@ +build = @build@ +build_alias = @build_alias@ +build_cpu = @build_cpu@ +build_os = @build_os@ +build_vendor = @build_vendor@ +builddir = @builddir@ +datadir = @datadir@ +datarootdir = @datarootdir@ +docdir = @docdir@ +dvidir = @dvidir@ +emacs_local_vars_begin = @emacs_local_vars_begin@ +emacs_local_vars_end = @emacs_local_vars_end@ +emacs_local_vars_read_only = @emacs_local_vars_read_only@ +exec_prefix = @exec_prefix@ +host = @host@ +host_alias = @host_alias@ +host_cpu = @host_cpu@ +host_os = @host_os@ +host_vendor = @host_vendor@ +htmldir = @htmldir@ +includedir = @includedir@ +infodir = @infodir@ +install_sh = @install_sh@ +libdir = @libdir@ +libexecdir = @libexecdir@ +localedir = @localedir@ +localstatedir = @localstatedir@ +mandir = @mandir@ +mkdir_p = @mkdir_p@ +oldincludedir = @oldincludedir@ +pdfdir = @pdfdir@ +pkgpyexecdir = @pkgpyexecdir@ +pkgpythondir = @pkgpythondir@ +prefix = @prefix@ +program_transform_name = @program_transform_name@ +psdir = @psdir@ +pyexecdir = @pyexecdir@ +pythondir = @pythondir@ +sbindir = @sbindir@ +sharedstatedir = @sharedstatedir@ +srcdir = @srcdir@ +sysconfdir = @sysconfdir@ +target_alias = @target_alias@ +top_build_prefix = @top_build_prefix@ +top_builddir = @top_builddir@ +top_srcdir = @top_srcdir@ +EXTRA_DIST = browsertest.html \ + index.html \ + longTests.html \ + Makefile.am \ + manifest.json \ + popup.html \ + popup.js \ + runbrowsertest.js \ + rununittests.js \ + setup_testing.js \ + testicon.png \ + testkey2.pub \ + testkey.pub \ + testkey.sec \ + tests/decryptTest.js \ + tests/encryptDecryptTest.js \ + tests/encryptTest.js \ + tests/inputvalues.js \ + tests/KeyImportExport.js \ + tests/KeyInfos.js \ + tests/longRunningTests.js \ + tests/signTest.js \ + tests/startup.js \ + tests/verifyTest.js \ + unittests.html + +all: all-am + +.SUFFIXES: +$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps) + @for dep in $?; do \ + case '$(am__configure_deps)' in \ + *$$dep*) \ + ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \ + && { if test -f $@; then exit 0; else break; fi; }; \ + exit 1;; \ + esac; \ + done; \ + echo ' cd $(top_srcdir) && $(AUTOMAKE) --gnu lang/js/BrowserTestExtension/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --gnu lang/js/BrowserTestExtension/Makefile +.PRECIOUS: Makefile +Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status + @case '$?' in \ + *config.status*) \ + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ + *) \ + echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe)'; \ + cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe);; \ + esac; + +$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh + +$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(am__aclocal_m4_deps): + +mostlyclean-libtool: + -rm -f *.lo + +clean-libtool: + -rm -rf .libs _libs +tags TAGS: + +ctags CTAGS: + +cscope cscopelist: + + +distdir: $(DISTFILES) + @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + list='$(DISTFILES)'; \ + dist_files=`for file in $$list; do echo $$file; done | \ + sed -e "s|^$$srcdirstrip/||;t" \ + -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ + case $$dist_files in \ + */*) $(MKDIR_P) `echo "$$dist_files" | \ + sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ + sort -u` ;; \ + esac; \ + for file in $$dist_files; do \ + if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ + if test -d $$d/$$file; then \ + dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ + if test -d "$(distdir)/$$file"; then \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ + cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ + else \ + test -f "$(distdir)/$$file" \ + || cp -p $$d/$$file "$(distdir)/$$file" \ + || exit 1; \ + fi; \ + done +check-am: all-am +check: check-am +all-am: Makefile +installdirs: +install: install-am +install-exec: install-exec-am +install-data: install-data-am +uninstall: uninstall-am + +install-am: all-am + @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am + +installcheck: installcheck-am +install-strip: + if test -z '$(STRIP)'; then \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + install; \ + else \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \ + fi +mostlyclean-generic: + +clean-generic: + +distclean-generic: + -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES) + -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES) + +maintainer-clean-generic: + @echo "This command is intended for maintainers to use" + @echo "it deletes files that may require special tools to rebuild." +clean: clean-am + +clean-am: clean-generic clean-libtool mostlyclean-am + +distclean: distclean-am + -rm -f Makefile +distclean-am: clean-am distclean-generic + +dvi: dvi-am + +dvi-am: + +html: html-am + +html-am: + +info: info-am + +info-am: + +install-data-am: + +install-dvi: install-dvi-am + +install-dvi-am: + +install-exec-am: + +install-html: install-html-am + +install-html-am: + +install-info: install-info-am + +install-info-am: + +install-man: + +install-pdf: install-pdf-am + +install-pdf-am: + +install-ps: install-ps-am + +install-ps-am: + +installcheck-am: + +maintainer-clean: maintainer-clean-am + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic + +mostlyclean: mostlyclean-am + +mostlyclean-am: mostlyclean-generic mostlyclean-libtool + +pdf: pdf-am + +pdf-am: + +ps: ps-am + +ps-am: + +uninstall-am: + +.MAKE: install-am install-strip + +.PHONY: all all-am check check-am clean clean-generic clean-libtool \ + cscopelist-am ctags-am distclean distclean-generic \ + distclean-libtool distdir dvi dvi-am html html-am info info-am \ + install install-am install-data install-data-am install-dvi \ + install-dvi-am install-exec install-exec-am install-html \ + install-html-am install-info install-info-am install-man \ + install-pdf install-pdf-am install-ps install-ps-am \ + install-strip installcheck installcheck-am installdirs \ + maintainer-clean maintainer-clean-generic mostlyclean \ + mostlyclean-generic mostlyclean-libtool pdf pdf-am ps ps-am \ + tags-am uninstall uninstall-am + + +# Tell versions [3.59,3.63) of GNU make to not export all variables. +# Otherwise a system limit (for SysV at least) may be exceeded. +.NOEXPORT: diff --git a/lang/js/BrowserTestExtension/browsertest.html b/lang/js/BrowserTestExtension/browsertest.html new file mode 100644 index 0000000..0d3e293 --- /dev/null +++ b/lang/js/BrowserTestExtension/browsertest.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <link href="libs/mocha.css" rel="stylesheet" /> + </head> +<body> + <h3>Browsertest</h3> + <div id="mocha"></div> + <!-- load unit tests --> + <script src="libs/mocha.js"></script> + <script src="libs/chai.js"></script> + <script src="setup_testing.js"></script> + <script src="libs/gpgmejs.bundle.js"></script> + <script src="tests/inputvalues.js"></script> +<!-- insert tests here--> + <script src="tests/startup.js"></script> + <script src="tests/KeyInfos.js"></script> + <script src="tests/encryptTest.js"></script> + <script src="tests/encryptDecryptTest.js"></script> + <script src="tests/signTest.js"></script> + <script src="tests/verifyTest.js"></script> + <script src="tests/decryptTest.js"></script> + <script src="tests/KeyImportExport.js"></script> +<!-- run tests --> + <script src="runbrowsertest.js"></script> + </body> +</html> diff --git a/lang/js/BrowserTestExtension/index.html b/lang/js/BrowserTestExtension/index.html new file mode 100644 index 0000000..7f8d97d --- /dev/null +++ b/lang/js/BrowserTestExtension/index.html @@ -0,0 +1,113 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <link href="libs/mocha.css" rel="stylesheet" /> + </head> +<body> + <h3>gpgmejs - Tests</h3> + <p> + The unittests rely on a separately packaged version of gpgmejs, + with the different classes and functions exposed. These tests and their + input values can be found in gpgme/lang/js/test. They do not test the + overall functionality, but the individual behaviour of the components. + <ul> + <li> + <a href="unittests.html"> + Unittests of the individual functions and classes. + </a> + </li> + </ul> + </p> + <p> + The functionality tests, to be found in + gpgme/lang/js/BrowserTestExtension, check the overall functionality of + the standard packaged version of gpgmejs. + </p> + <p> + Most tests rely on a test gpg key to be available in gpg, which can be + found at the bottom of this page, or as "testkey.sec" in the + BrowserTestExtension's directory. Please import this key to your tested + gpg installation, or adapt the input defined in tests/inputvalues.js + if you want to use different values. + </p> + <p> + <ul> + <li> + <a href="browsertest.html"> + Functionality tests using the bundled library. + </a> + </li> + <li> + <a href="longTests.html"> + Functionality tests with larger/longer running data sets. + </a> + </li> + </ul> + </p> + <hr /> + <p> + + <textarea rows="5" cols="65" wrap="hard" readonly> +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lQOYBFrsKEkBCADKw4Wt8J6M/88qD8PO6lSMCxH1cpwH8iK0uPaFFYsJkkXo7kWf +PTAtrV+REqF/o80dvYcdLvRsV21pvncZz/HXLu1yQ18mC3XObrKokbdgrTTKA5XE +BZkNsqyaMMJauT18H4hYkSg62/tTdO1cu/zWv/LFf7Xyn6+uA74ovXCJlO1s0N2c +PShtr98QRzPMf2owgVk37JnDNp4gGVDGHxSZOuUwxgYAZYnA8SFc+c+3ZrQfY870 ++O4j3Mz4p7yD13AwP4buQLBsb/icxekeQCqpRJhLH9f7MdEcGXa1x36RcEkHdu+M +yJ392eMgD+dKNfRCtyTPhjZTxvbNELIBYICfABEBAAEAB/wLJ0gyMjs2fFfT83wM +5Lzz2yQIwV4t3bblBAujdHTqeN5Zmsm/oakFyjSokULK96Kv0R4ej9eoIgMFvxFk +HRkrggxTrbsNJ7I6QcKYHTPeIIj318ykNL6fj0WJUcdPIENukXl5jbqNyk3/4D2y +TTDySyq6jHTgvMH4K4KJUSpglvSJPntTk9RhuFGHAF+sNR9atygDYctAaERMRtSg +LCoSt/AoX5GRMlQjXT9oqQjwSQoZyF4s8HMC8wdTFIE/E0L4IVdHVp8sz2UszNtT +W/evmCA+KVruKjRH/Fhrq4hHkEamW28+j4L6uAyagONP7BONs+S5Oo2zTT9+tV2R +ILTZBADdgLuAgF6C5Lu9jCF6DfFgaT/uafMyQNkEGNlxOHMWHTgLHe475V2eG9gA +amd4yXKyEFKU1PWnvlGuicQSGdzVcwmq61msvXgYD0FK3LP3yWzKnE4X1tzrC9Vp +/uHJxKjewCuyt1f5in919v+T8TbUxBYKC0zX/qWtX+10cTx77QQA6leqhToJ95Yc +u4UBrKMEO+y2v8Svb3LG7yI5oY8tkw0EkJ/kpZ8xTAfZYCe6fXdvVE3PHg2lrxyc +Wv/EU3QY/qA3G82mbXYeJ2jNZaTNYo4MylMrt4Mx25x4ke7JlsE8SVrQ+4CrHkqp +OjSIa7fppLrQ78uW980AtN8NNQGrlTsD/A9aoA60Igxy1Q3K2uSyDCyjLknv57ym +ZSBD3/t7m0l6Q6gbdfhNGosT+Hd4y3actqEqzXZHW2VG4dKZ/wRNkxtSm9adU9vs +EHyzxjb6mKIH32zAG5TaFT20hC+NK6lsyHr9UE2ZrS6ma2sLxGW2O40hqNsdD+5m +NrqeBc2I/js1PMK0EHRlc3RAZXhhbXBsZS5vcmeJAVQEEwEIAD4WIQTUFzW5Ejb9 +uIIEjFojAWNe7/DLBQUCWuwoSQIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIe +AQIXgAAKCRAjAWNe7/DLBf9kB/wOQ/S60HGwFq07W9N01HWULyhHKoMmcHL6rfZ6 +4oDqLxolPSasz7WAMW1jN4qtWJ0mFzwO83V6kaBe+wF6Kqir6udFSBW9rPcFg6/V +ZXPltT0a6uacIHq6DyQ5iMW4YQWbVy9OR2rNGkYo1JCBR0XdRJYCSX3yB4TWv/eX +nZ37/WjmiTOIZh35rjs+NuU/S5JPDfAp2/k70DevQeBsv+UjVXjWpNTZmPbvDnd9 +95uSmC6UY4hzyP84ORYMYn9n1QAR0goxDN6UunOf9Rlp1oMzdxMool/d1MlCxg2h +3jheuhv7lgUF4KpvHOuEPXQ7UO417E0TYcDZ1J8Nsv87SZeEnQOYBFrsKEkBCADj +oEBhG/QPqZHg8VyoD1xYRAWGxyDJkX/GrSs6yE+x2hk5FoQCajxKa/d4AVxOnJpd +whAfeXeSNaql5Ejgzax+Tdj9BV6vtGVJVv0pO7bgAiZxkA6RHxtNqhpPnPQoXvUz +kzpRgpuL+Nj4yIg7z1ITH6KQH4u5SI9vd+j/8i9Taz67pdZwuJjac8qBuJHjzAo1 +bjYctFYUSG5pbmMQyNLySzgiNkFa4DajODlt3RuqVGP316Fk+Sy2+60tC/HlX8jg +MyMONfOGBQx6jk8tvAphS/LAqrrNepnagIyLUGKU+L8cB2g1PGGp2biBFWqZbudZ +oyRBet/0yH/zirBdQJw1ABEBAAEAB/4lN3gXOI4OuoOcsvHak4pebx61Mt0YP9cT +qZASIBqxok5x8E28pFh/tYfkYdqRCtdNYZOnxcEoUWh5j6nfwZkEnJ9P/T8GPNk7 +pMKnKXmExi05b5uGHD8nU1rSbf/YkvAF0vpbxd4/RDxbbtQhbUwGzusSI+pBLM0w +5TreEB+vRGBc2gOvXXOtKLNEa7M9rH2EwbAkP3jOGGwgk6adxbQdBcRxq4merqhL +YrVz73bCj8TDc0fsNJyIaZZJ++ejfBFYavsF1pvx9z7FNFi8rSXoiB3SBtaWGfhr +bwNaMZrDc7TRIq/fgGaL6g//bzcWrr1YaHXZ10Bgx6UymDOlYkCpBADm0Hv46sPw +07SO8+IACcaQliOto1pndOPwTimCeo58/7rf8I2a5uuJloGrnPwAX65bKDnUALp6 +X3lnXRNMhnB3Uewx4i00LQmjsxhJfQiGLpMv0j58tn64s7GqQzGVV1JKcQm992RV +jFOydyjZ+K4LGWEOITG/bZrMEVNGCM+OnQQA/Haz8xN0NFSlq7tyfFc0pkx/TiCX +xGfBqbO0wU2b5GMnZbY/06HENpidIzpa231VQaw5/nPTvfhlLKW1iGAkc148cX1q +lL9w2ksXuaHR3LXud2VcfVTIdxU/7h7u1dD/85+c0+7jlGObD9cXKxlM6OjpIJz1 +l5/1h3C5S0TuxHkEAL/3BGihkhNfv1Xx0rWu0/732usX/nE/A9C26hGu41FUf3fp +0ilonKpKZUEwWt5hWSEFCSrznNVekiO0rxvuu3RVegvzThPNU4Pf4JZtJpRVhvUQ +d9ulxJw7V9rs75uNBatTNC0kXuGoXhehw4Bn93xa67gYGd3LfrH+oT0GCDpTSHCJ +ATwEGAEIACYWIQTUFzW5Ejb9uIIEjFojAWNe7/DLBQUCWuwoSQIbDAUJA8JnAAAK +CRAjAWNe7/DLBf0pCACPp5hBuUWngu2Hqvg+tNiujfsiYzId3MffFxEk3CbXeHcJ +5F32NDJ9PYCnra4L8wSv+NZt9gIa8lFwoFSFQCjzH7KE86XcV3MhfdJTNb/+9CR7 +Jq3e/4Iy0N5ip7PNYMCyakcAsxvsNCJKrSaDuYe/OAoTXRBtgRWE2uyT315em02L +kr+2Cc/Qk6H+vlNOHGRgnpI/OZZjnUuUfBUvMGHr1phW+y7aeymC9PnUGdViRdJe +23nntMSDA+0/I7ESO9JsWvJbyBmuiZpu9JjScOjYH9xpQLqRNyw4WHpZriN69F0t +9Mmd7bM1+UyPgbPEr0iWMeyctYsuOLeUyQKMscDT +=hkUm +-----END PGP PRIVATE KEY BLOCK----- + </textarea> + + </p> + </body> +</html> diff --git a/lang/js/BrowserTestExtension/longTests.html b/lang/js/BrowserTestExtension/longTests.html new file mode 100644 index 0000000..8ff969b --- /dev/null +++ b/lang/js/BrowserTestExtension/longTests.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <link href="libs/mocha.css" rel="stylesheet" /> + </head> +<body> + <h3>Browsertest</h3> + <div id="mocha"></div> + <!-- load unit tests --> + <script src="libs/mocha.js"></script> + <script src="libs/chai.js"></script> + <script src="setup_testing.js"></script> + <script src="libs/gpgmejs.bundle.js"></script> + <script src="tests/inputvalues.js"></script> +<!-- insert tests here--> + <script src="tests/startup.js"></script> + <script src="tests/longRunningTests.js"></script> +<!-- run tests --> + <script src="runbrowsertest.js"></script> + </body> +</html> diff --git a/lang/js/BrowserTestExtension/manifest.json b/lang/js/BrowserTestExtension/manifest.json new file mode 100644 index 0000000..a9e605b --- /dev/null +++ b/lang/js/BrowserTestExtension/manifest.json @@ -0,0 +1,13 @@ +{ + "manifest_version": 2, + + "name": "Browsertests for gpgmejs", + "description": "Run the browsertests.", + "version": "0.1", + "content_security_policy": "default-src 'self' filesystem:", + "browser_action": { + "default_icon": "testicon.png", + "default_popup": "popup.html" + }, + "permissions": ["nativeMessaging", "activeTab"] + } diff --git a/lang/js/BrowserTestExtension/popup.html b/lang/js/BrowserTestExtension/popup.html new file mode 100644 index 0000000..f17f262 --- /dev/null +++ b/lang/js/BrowserTestExtension/popup.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <script src="popup.js"></script> + </head> + <body> + </body> +</html>
\ No newline at end of file diff --git a/lang/js/BrowserTestExtension/popup.js b/lang/js/BrowserTestExtension/popup.js new file mode 100644 index 0000000..794620b --- /dev/null +++ b/lang/js/BrowserTestExtension/popup.js @@ -0,0 +1,30 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +/* global chrome */ + +document.addEventListener('DOMContentLoaded', function() { + chrome.tabs.create({ + url: './index.html' + }); +}); diff --git a/lang/js/BrowserTestExtension/runbrowsertest.js b/lang/js/BrowserTestExtension/runbrowsertest.js new file mode 100644 index 0000000..c46eb12 --- /dev/null +++ b/lang/js/BrowserTestExtension/runbrowsertest.js @@ -0,0 +1,26 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +/* global mocha */ + +mocha.run(); diff --git a/lang/js/BrowserTestExtension/rununittests.js b/lang/js/BrowserTestExtension/rununittests.js new file mode 100644 index 0000000..df31589 --- /dev/null +++ b/lang/js/BrowserTestExtension/rununittests.js @@ -0,0 +1,27 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +/* global Gpgmejs_test, mocha*/ + +Gpgmejs_test.unittests(); +mocha.run(); diff --git a/lang/js/BrowserTestExtension/setup_testing.js b/lang/js/BrowserTestExtension/setup_testing.js new file mode 100644 index 0000000..52aeac5 --- /dev/null +++ b/lang/js/BrowserTestExtension/setup_testing.js @@ -0,0 +1,28 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +/* global mocha, chai */ + +mocha.setup('bdd'); +const expect = chai.expect; //eslint-disable-line no-unused-vars +chai.config.includeStack = true;
\ No newline at end of file diff --git a/lang/js/BrowserTestExtension/testicon.png b/lang/js/BrowserTestExtension/testicon.png Binary files differnew file mode 100644 index 0000000..a98463d --- /dev/null +++ b/lang/js/BrowserTestExtension/testicon.png diff --git a/lang/js/BrowserTestExtension/testkey.pub b/lang/js/BrowserTestExtension/testkey.pub new file mode 100644 index 0000000..cfc329f --- /dev/null +++ b/lang/js/BrowserTestExtension/testkey.pub @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFrsKEkBCADKw4Wt8J6M/88qD8PO6lSMCxH1cpwH8iK0uPaFFYsJkkXo7kWf +PTAtrV+REqF/o80dvYcdLvRsV21pvncZz/HXLu1yQ18mC3XObrKokbdgrTTKA5XE +BZkNsqyaMMJauT18H4hYkSg62/tTdO1cu/zWv/LFf7Xyn6+uA74ovXCJlO1s0N2c +PShtr98QRzPMf2owgVk37JnDNp4gGVDGHxSZOuUwxgYAZYnA8SFc+c+3ZrQfY870 ++O4j3Mz4p7yD13AwP4buQLBsb/icxekeQCqpRJhLH9f7MdEcGXa1x36RcEkHdu+M +yJ392eMgD+dKNfRCtyTPhjZTxvbNELIBYICfABEBAAG0EHRlc3RAZXhhbXBsZS5v +cmeJAVQEEwEIAD4WIQTUFzW5Ejb9uIIEjFojAWNe7/DLBQUCWuwoSQIbAwUJA8Jn +AAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRAjAWNe7/DLBf9kB/wOQ/S60HGw +Fq07W9N01HWULyhHKoMmcHL6rfZ64oDqLxolPSasz7WAMW1jN4qtWJ0mFzwO83V6 +kaBe+wF6Kqir6udFSBW9rPcFg6/VZXPltT0a6uacIHq6DyQ5iMW4YQWbVy9OR2rN +GkYo1JCBR0XdRJYCSX3yB4TWv/eXnZ37/WjmiTOIZh35rjs+NuU/S5JPDfAp2/k7 +0DevQeBsv+UjVXjWpNTZmPbvDnd995uSmC6UY4hzyP84ORYMYn9n1QAR0goxDN6U +unOf9Rlp1oMzdxMool/d1MlCxg2h3jheuhv7lgUF4KpvHOuEPXQ7UO417E0TYcDZ +1J8Nsv87SZeEuQENBFrsKEkBCADjoEBhG/QPqZHg8VyoD1xYRAWGxyDJkX/GrSs6 +yE+x2hk5FoQCajxKa/d4AVxOnJpdwhAfeXeSNaql5Ejgzax+Tdj9BV6vtGVJVv0p +O7bgAiZxkA6RHxtNqhpPnPQoXvUzkzpRgpuL+Nj4yIg7z1ITH6KQH4u5SI9vd+j/ +8i9Taz67pdZwuJjac8qBuJHjzAo1bjYctFYUSG5pbmMQyNLySzgiNkFa4DajODlt +3RuqVGP316Fk+Sy2+60tC/HlX8jgMyMONfOGBQx6jk8tvAphS/LAqrrNepnagIyL +UGKU+L8cB2g1PGGp2biBFWqZbudZoyRBet/0yH/zirBdQJw1ABEBAAGJATwEGAEI +ACYWIQTUFzW5Ejb9uIIEjFojAWNe7/DLBQUCWuwoSQIbDAUJA8JnAAAKCRAjAWNe +7/DLBf0pCACPp5hBuUWngu2Hqvg+tNiujfsiYzId3MffFxEk3CbXeHcJ5F32NDJ9 +PYCnra4L8wSv+NZt9gIa8lFwoFSFQCjzH7KE86XcV3MhfdJTNb/+9CR7Jq3e/4Iy +0N5ip7PNYMCyakcAsxvsNCJKrSaDuYe/OAoTXRBtgRWE2uyT315em02Lkr+2Cc/Q +k6H+vlNOHGRgnpI/OZZjnUuUfBUvMGHr1phW+y7aeymC9PnUGdViRdJe23nntMSD +A+0/I7ESO9JsWvJbyBmuiZpu9JjScOjYH9xpQLqRNyw4WHpZriN69F0t9Mmd7bM1 ++UyPgbPEr0iWMeyctYsuOLeUyQKMscDT +=QyY6 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/lang/js/BrowserTestExtension/testkey.sec b/lang/js/BrowserTestExtension/testkey.sec new file mode 100644 index 0000000..ced8f3e --- /dev/null +++ b/lang/js/BrowserTestExtension/testkey.sec @@ -0,0 +1,57 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lQOYBFrsKEkBCADKw4Wt8J6M/88qD8PO6lSMCxH1cpwH8iK0uPaFFYsJkkXo7kWf +PTAtrV+REqF/o80dvYcdLvRsV21pvncZz/HXLu1yQ18mC3XObrKokbdgrTTKA5XE +BZkNsqyaMMJauT18H4hYkSg62/tTdO1cu/zWv/LFf7Xyn6+uA74ovXCJlO1s0N2c +PShtr98QRzPMf2owgVk37JnDNp4gGVDGHxSZOuUwxgYAZYnA8SFc+c+3ZrQfY870 ++O4j3Mz4p7yD13AwP4buQLBsb/icxekeQCqpRJhLH9f7MdEcGXa1x36RcEkHdu+M +yJ392eMgD+dKNfRCtyTPhjZTxvbNELIBYICfABEBAAEAB/wLJ0gyMjs2fFfT83wM +5Lzz2yQIwV4t3bblBAujdHTqeN5Zmsm/oakFyjSokULK96Kv0R4ej9eoIgMFvxFk +HRkrggxTrbsNJ7I6QcKYHTPeIIj318ykNL6fj0WJUcdPIENukXl5jbqNyk3/4D2y +TTDySyq6jHTgvMH4K4KJUSpglvSJPntTk9RhuFGHAF+sNR9atygDYctAaERMRtSg +LCoSt/AoX5GRMlQjXT9oqQjwSQoZyF4s8HMC8wdTFIE/E0L4IVdHVp8sz2UszNtT +W/evmCA+KVruKjRH/Fhrq4hHkEamW28+j4L6uAyagONP7BONs+S5Oo2zTT9+tV2R +ILTZBADdgLuAgF6C5Lu9jCF6DfFgaT/uafMyQNkEGNlxOHMWHTgLHe475V2eG9gA +amd4yXKyEFKU1PWnvlGuicQSGdzVcwmq61msvXgYD0FK3LP3yWzKnE4X1tzrC9Vp +/uHJxKjewCuyt1f5in919v+T8TbUxBYKC0zX/qWtX+10cTx77QQA6leqhToJ95Yc +u4UBrKMEO+y2v8Svb3LG7yI5oY8tkw0EkJ/kpZ8xTAfZYCe6fXdvVE3PHg2lrxyc +Wv/EU3QY/qA3G82mbXYeJ2jNZaTNYo4MylMrt4Mx25x4ke7JlsE8SVrQ+4CrHkqp +OjSIa7fppLrQ78uW980AtN8NNQGrlTsD/A9aoA60Igxy1Q3K2uSyDCyjLknv57ym +ZSBD3/t7m0l6Q6gbdfhNGosT+Hd4y3actqEqzXZHW2VG4dKZ/wRNkxtSm9adU9vs +EHyzxjb6mKIH32zAG5TaFT20hC+NK6lsyHr9UE2ZrS6ma2sLxGW2O40hqNsdD+5m +NrqeBc2I/js1PMK0EHRlc3RAZXhhbXBsZS5vcmeJAVQEEwEIAD4WIQTUFzW5Ejb9 +uIIEjFojAWNe7/DLBQUCWuwoSQIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIe +AQIXgAAKCRAjAWNe7/DLBf9kB/wOQ/S60HGwFq07W9N01HWULyhHKoMmcHL6rfZ6 +4oDqLxolPSasz7WAMW1jN4qtWJ0mFzwO83V6kaBe+wF6Kqir6udFSBW9rPcFg6/V +ZXPltT0a6uacIHq6DyQ5iMW4YQWbVy9OR2rNGkYo1JCBR0XdRJYCSX3yB4TWv/eX +nZ37/WjmiTOIZh35rjs+NuU/S5JPDfAp2/k70DevQeBsv+UjVXjWpNTZmPbvDnd9 +95uSmC6UY4hzyP84ORYMYn9n1QAR0goxDN6UunOf9Rlp1oMzdxMool/d1MlCxg2h +3jheuhv7lgUF4KpvHOuEPXQ7UO417E0TYcDZ1J8Nsv87SZeEnQOYBFrsKEkBCADj +oEBhG/QPqZHg8VyoD1xYRAWGxyDJkX/GrSs6yE+x2hk5FoQCajxKa/d4AVxOnJpd +whAfeXeSNaql5Ejgzax+Tdj9BV6vtGVJVv0pO7bgAiZxkA6RHxtNqhpPnPQoXvUz +kzpRgpuL+Nj4yIg7z1ITH6KQH4u5SI9vd+j/8i9Taz67pdZwuJjac8qBuJHjzAo1 +bjYctFYUSG5pbmMQyNLySzgiNkFa4DajODlt3RuqVGP316Fk+Sy2+60tC/HlX8jg +MyMONfOGBQx6jk8tvAphS/LAqrrNepnagIyLUGKU+L8cB2g1PGGp2biBFWqZbudZ +oyRBet/0yH/zirBdQJw1ABEBAAEAB/4lN3gXOI4OuoOcsvHak4pebx61Mt0YP9cT +qZASIBqxok5x8E28pFh/tYfkYdqRCtdNYZOnxcEoUWh5j6nfwZkEnJ9P/T8GPNk7 +pMKnKXmExi05b5uGHD8nU1rSbf/YkvAF0vpbxd4/RDxbbtQhbUwGzusSI+pBLM0w +5TreEB+vRGBc2gOvXXOtKLNEa7M9rH2EwbAkP3jOGGwgk6adxbQdBcRxq4merqhL +YrVz73bCj8TDc0fsNJyIaZZJ++ejfBFYavsF1pvx9z7FNFi8rSXoiB3SBtaWGfhr +bwNaMZrDc7TRIq/fgGaL6g//bzcWrr1YaHXZ10Bgx6UymDOlYkCpBADm0Hv46sPw +07SO8+IACcaQliOto1pndOPwTimCeo58/7rf8I2a5uuJloGrnPwAX65bKDnUALp6 +X3lnXRNMhnB3Uewx4i00LQmjsxhJfQiGLpMv0j58tn64s7GqQzGVV1JKcQm992RV +jFOydyjZ+K4LGWEOITG/bZrMEVNGCM+OnQQA/Haz8xN0NFSlq7tyfFc0pkx/TiCX +xGfBqbO0wU2b5GMnZbY/06HENpidIzpa231VQaw5/nPTvfhlLKW1iGAkc148cX1q +lL9w2ksXuaHR3LXud2VcfVTIdxU/7h7u1dD/85+c0+7jlGObD9cXKxlM6OjpIJz1 +l5/1h3C5S0TuxHkEAL/3BGihkhNfv1Xx0rWu0/732usX/nE/A9C26hGu41FUf3fp +0ilonKpKZUEwWt5hWSEFCSrznNVekiO0rxvuu3RVegvzThPNU4Pf4JZtJpRVhvUQ +d9ulxJw7V9rs75uNBatTNC0kXuGoXhehw4Bn93xa67gYGd3LfrH+oT0GCDpTSHCJ +ATwEGAEIACYWIQTUFzW5Ejb9uIIEjFojAWNe7/DLBQUCWuwoSQIbDAUJA8JnAAAK +CRAjAWNe7/DLBf0pCACPp5hBuUWngu2Hqvg+tNiujfsiYzId3MffFxEk3CbXeHcJ +5F32NDJ9PYCnra4L8wSv+NZt9gIa8lFwoFSFQCjzH7KE86XcV3MhfdJTNb/+9CR7 +Jq3e/4Iy0N5ip7PNYMCyakcAsxvsNCJKrSaDuYe/OAoTXRBtgRWE2uyT315em02L +kr+2Cc/Qk6H+vlNOHGRgnpI/OZZjnUuUfBUvMGHr1phW+y7aeymC9PnUGdViRdJe +23nntMSDA+0/I7ESO9JsWvJbyBmuiZpu9JjScOjYH9xpQLqRNyw4WHpZriN69F0t +9Mmd7bM1+UyPgbPEr0iWMeyctYsuOLeUyQKMscDT +=hkUm +-----END PGP PRIVATE KEY BLOCK----- diff --git a/lang/js/BrowserTestExtension/testkey2.pub b/lang/js/BrowserTestExtension/testkey2.pub new file mode 100644 index 0000000..557bd5b --- /dev/null +++ b/lang/js/BrowserTestExtension/testkey2.pub @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFsMHecBCACqdJgqa+CeNYwPCK+MpOwAV6uFVjDyO2LmOs6+XfDWRBU/Zjtz +8zdYNKSbLjkWN4ujV5aiyA7MtEofszzYLEoKUt1wiDScHMpW8qmEFDvl9g26MeAV +rTno9D5KodHvEIs8wnrqBs8ix0WLbh6J1Dtt8HQgIbN+v3gaRQrgBFe6z2ZYpHHx +ZfOu3iFKlm2WE/NekRkvvFIo3ApGvRhGIYw6JMmugBlo7s5xosJK0I9dkPGlEEtt +aF1RkcMj8sWG9vHAXcjlGgFfXSN9YLppydXpkuZGm4+gjLB2a3rbQCZVFnxCyG4O +ybjkP8Jw6Udm89bK2ucYFfjdrmYn/nJqRxeNABEBAAG0I1Rlc3QgTm9Qcml2S2V5 +IDxub2JvZHlAZXhhbXBsZS5vcmc+iQFOBBMBCAA4FiEE4Fmh4IZtMa4TEXCITZou +EzBBU9EFAlsMHecCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQTZouEzBB +U9F+qwf/SHj4uRnTWgyJ71FBxQDYCBq3jbi6e7hMkRPbJyJdnPIMAb2p0PJjBgjW +0pp4+kDPZans3UDHbma1u/SFI4/y6isJiK94Bk5xp5YliLGnUceTjgDFe6lBhfQ1 +zVWZC/NF3tPgbziIxXQTNt34nS+9dbV/QFDLW0POcN7C0jR/hgkBjMEH2PezWhSj +mL/yLfLfUYAoxVpXjfC5aPJKqw0tR7m5ibznjCphE+FUMRg8EOmJcg6soeJ5QspU +k2dPN3+Y0zCTNRgAHEI+yIQbM6pio6v2c+UCtT1QhW4xSI38/kcEG8QiM55r1TUy +FcWAY5n5t1nNZtMxxse3LqEon3rKiLkBDQRbDB3nAQgAqfAjSjcngERtM+ZYOwN0 +QF2v2FuEuMe8mhju7Met7SN2zGv1LnjhTNshEa9IABEfjZirE2Tqx4xCWDwDedK4 +u1ToFvcnuAMnq2O47Sh+eTypsf6WPFtPBWf6ctKY31hFXjgoyDBULBvl43XU/D9C +Mt7nsKDPYHVrrnge/qWPYVcb+cO0sSwNImMcwQSdTQ3VBq7MeNS9ZeBcXi+XCjhN +kjNum2AQqpkHHDQV7871yQ8RIILvZSSfkLb0/SNDU+bGaw2G3lcyKdIfZi2EWWZT +oCbH38I/+LV7nAEe4zFpHwW8X0Dkx2aLgxe6UszDH9L3eGhTLpJhOSiaanG+zZKm ++QARAQABiQE2BBgBCAAgFiEE4Fmh4IZtMa4TEXCITZouEzBBU9EFAlsMHecCGwwA +CgkQTZouEzBBU9H5TQgAolWvIsez/WW8N2tmZEnX0LOFNB+1S4L4X983njwNdoVI +w19pbj+8RIHF/H9kcPGi7jK96gvlykQn3uez/95D2AiRFW5KYdOouFisKgHpv8Ay +BrhclHv11yK+X/0iTD0scYaG7np5162xLkaxSO9hsz2fGv20RKaXCWkI69fWw0BR +XlI5pZh2YFei2ZhH/tIMIW65h3w0gtgaZBBdpZTOOW4zvghyN+0MSObqkI1BvUJu +caDFI4d6ZTmp5SY+pZyktZ4bg/vMH5VFxdIKgbLx9uVeTvOupvbAW0TNulYGUBQE +nm+S0zr3W18t64e4sS3oHse8zCqo1iiImpba6F1Oaw== +=y6DD +-----END PGP PUBLIC KEY BLOCK----- diff --git a/lang/js/BrowserTestExtension/tests/KeyImportExport.js b/lang/js/BrowserTestExtension/tests/KeyImportExport.js new file mode 100644 index 0000000..b3d95bb --- /dev/null +++ b/lang/js/BrowserTestExtension/tests/KeyImportExport.js @@ -0,0 +1,152 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + * Raimund Renkert <rrenkert@intevation.de> + */ + +/* global describe, it, expect, before, afterEach, Gpgmejs*/ +/* global ImportablePublicKey, inputvalues */ + +describe('Key importing', function () { + const fpr = ImportablePublicKey.fingerprint; + const pubKey = ImportablePublicKey.key; + const changedKey = ImportablePublicKey.keyChangedUserId; + + let context = null; + before(function (done){ + const prm = Gpgmejs.init({ timeout: 2000 }); + prm.then(function (gpgmejs){ + context = gpgmejs; + context.Keyring.getKeys({ pattern: fpr }).then( + function (result){ + if (result.length === 1) { + result[0].delete().then(function (){ + done(); + },function (){ + done(); + }); + } else { + done(); + } + }); + }); + }); + + afterEach(function (done){ + // delete the test key if still present + context.Keyring.getKeys({ pattern: fpr }).then( + function (result){ + if (result.length === 1) { + result[0].delete().then(function (){ + done(); + },function (){ + done(); + }); + } else { + done(); + } + }); + }); + + it('Importing Key', function (done) { + context.Keyring.getKeys({ pattern: fpr }).then(function (result){ + expect(result).to.be.an('array'); + expect(result.length).to.equal(0); + context.Keyring.importKey(pubKey).then(function (result){ + expect(result.Keys).to.be.an('array'); + expect(result.Keys[0]).to.not.be.undefined; + expect(result.Keys[0].key).to.be.an('object'); + expect(result.Keys[0].key.fingerprint).to.equal(fpr); + expect(result.Keys[0].status).to.equal('newkey'); + expect(result.summary.considered).to.equal(1); + expect(result.summary.imported).to.equal(1); + done(); + }); + }); + }); + + it('Updating Key', function (done){ + context.Keyring.importKey(pubKey) + .then(function (result){ + expect(result.Keys[0].key).to.not.be.undefined; + expect(result.Keys[0].status).to.equal('newkey'); + context.Keyring.importKey(changedKey).then(function (res){ + expect(res.Keys[0].key).to.be.an('object'); + expect(res.Keys[0].key.fingerprint).to.equal(fpr); + expect(res.Keys[0].status).to.equal('change'); + expect(res.Keys[0].changes.userId).to.be.true; + expect(res.Keys[0].changes.subkey).to.be.false; + expect(res.Keys[0].changes.signature).to.be.true; + expect(res.summary.considered).to.equal(1); + done(); + }); + }); + }); + + it('Deleting Key', function (done) { + context.Keyring.importKey(pubKey).then(function (result){ + expect(result.Keys[0].key).to.be.an('object'); + expect(result.Keys[0].key.fingerprint).to.equal(fpr); + result.Keys[0].key.delete().then(function (result){ + expect(result).to.be.true; + done(); + }); + }); + }); + + it('Import result feedback', function (done){ + context.Keyring.importKey(pubKey, true).then(function (result){ + expect(result).to.be.an('object'); + expect(result.Keys[0]).to.be.an('object'); + expect(result.Keys[0].key.fingerprint).to.equal(fpr); + expect(result.Keys[0].status).to.equal('newkey'); + result.Keys[0].key.getArmor().then(function (armor){ + expect(armor).to.be.a('string'); + done(); + }); + }); + }); + + it('exporting armored Key with getKeysArmored', function (done) { + context.Keyring.importKey(pubKey).then(function (){ + context.Keyring.getKeysArmored({ pattern: fpr }) + .then(function (result){ + expect(result).to.be.an('object'); + expect(result.armored).to.be.a('string'); + expect(result.secret_fprs).to.be.undefined; + done(); + }); + }); + }); + + it('Exporting Key (including secret fingerprints)', function (done) { + const key_secret = inputvalues.encrypt.good.fingerprint; + context.Keyring.getKeysArmored({ + pattern: key_secret, with_secret_fpr: true }) + .then(function (result){ + expect(result).to.be.an('object'); + expect(result.armored).to.be.a('string'); + expect(result.secret_fprs).to.be.an('array'); + expect(result.secret_fprs[0]).to.equal(key_secret); + done(); + }); + }); +});
\ No newline at end of file diff --git a/lang/js/BrowserTestExtension/tests/KeyInfos.js b/lang/js/BrowserTestExtension/tests/KeyInfos.js new file mode 100644 index 0000000..d122958 --- /dev/null +++ b/lang/js/BrowserTestExtension/tests/KeyInfos.js @@ -0,0 +1,59 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +/* global describe, it, expect, before, Gpgmejs */ +/* global inputvalues*/ + +describe('Key information', function () { + let context = null; + before(function (done){ + const prm = Gpgmejs.init({ timeout: 2000 }); + prm.then(function (gpgmejs){ + context = gpgmejs; + done(); + }); + }); + + it('A fingerprint is consistently returned upper case hex', function (done){ + const mixedCase = inputvalues.encrypt.good.fingerprint_mixedcase; + context.Keyring.getKeys({ pattern: mixedCase }).then(function (result){ + expect(result).to.be.an('array'); + expect(result.length).to.equal(1); + expect(result[0].fingerprint).to.equal(mixedCase.toUpperCase()); + done(); + }); + }); + + it('A userId keeps their encoding', function (done){ + context.Keyring.importKey(inputvalues.publicKeyNonAscii.key, true) + .then(function (result){ + expect(result.Keys[0]).to.be.an('object'); + const user = result.Keys[0].key.get('userids')[0]; + expect(user.get('name')).to.equal( + inputvalues.publicKeyNonAscii.userid); + result.Keys[0].key.delete().then(function (){ + done(); + }); + }); + }); +}); diff --git a/lang/js/BrowserTestExtension/tests/decryptTest.js b/lang/js/BrowserTestExtension/tests/decryptTest.js new file mode 100644 index 0000000..5817eb4 --- /dev/null +++ b/lang/js/BrowserTestExtension/tests/decryptTest.js @@ -0,0 +1,114 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +/* global describe, it, before, expect, Gpgmejs */ +/* global bigString, inputvalues, sabotageMsg, binaryData, filename_files */ + +describe('Decryption', function () { + let context = null; + const good_fpr = inputvalues.encrypt.good.fingerprint; + + before(function (done){ + const prm = Gpgmejs.init({ timeout:2000 }); + prm.then(function (gpgmejs){ + context = gpgmejs; + done(); + }); + }); + + it('Decryption of random string fails', function (done) { + let data = bigString(20 * 1024); + context.decrypt({ data: data }).then( + function (){}, + function (error){ + expect(error).to.be.an('error'); + expect(error.code).to.equal('GNUPG_ERROR'); + done(); + }); + }); + + it('Decryption of slightly corrupted message fails', function (done) { + const data = bigString(10000); + context.encrypt({ data: data, publicKeys:good_fpr }).then( + function (enc){ + context.decrypt({ data: sabotageMsg(enc.data) }).then( + function (){}, + function (error){ + expect(error).to.be.an('error'); + expect(error.code).to.equal('GNUPG_ERROR'); + done(); + }); + }); + }).timeout(5000); + + + it('decrypt/verify operations return proper information', function (done){ + const data = inputvalues.encryptSignedMessage; + context.decrypt({ data: data }).then(function (result){ + expect(result).to.be.an('object'); + expect(result.signatures).to.be.an('object'); + expect(result.signatures.all_valid).to.be.true; + expect(result.signatures.count).to.equal(1); + expect(result.signatures.signatures.good).to.be.an('array'); + expect( + result.signatures.signatures.good[0].fingerprint).to.equal( + good_fpr); + done(); + }); + }); + + it('decrypt of a png, result as base64', function (done){ + const data = binaryData.encryptedArmored; + context.decrypt({ data: data, expect: 'base64' }) + .then(function (result){ + expect(result.data).to.be.a('String'); + expect(result.data).to.equal(binaryData.base64); + expect(result.format).to.equal('base64'); + done(); + }); + }); + + it('decrypt of a png, result as Uint8Array', function (done){ + const data = binaryData.encryptedArmored; + context.decrypt({ data: data, expect: 'uint8' }) + .then(function (result){ + expect(result.data).to.be.an('Uint8Array'); + expect(result.format).to.equal('uint8'); + done(); + }); + }); + + for (let i=0; i < filename_files.length; i++) { + it ( + 'decrypted file_names keep correct encoding (' + i + ')', + function (done){ + context.decrypt({ data:filename_files[i].data }) + .then(function (answer){ + expect(answer.file_name).to.equal( + filename_files[i].name); + done(); + }); + }); + } + +});
\ No newline at end of file diff --git a/lang/js/BrowserTestExtension/tests/encryptDecryptTest.js b/lang/js/BrowserTestExtension/tests/encryptDecryptTest.js new file mode 100644 index 0000000..b4e4f3f --- /dev/null +++ b/lang/js/BrowserTestExtension/tests/encryptDecryptTest.js @@ -0,0 +1,200 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +/* global describe, it, expect, before, Gpgmejs */ +/* global inputvalues, encryptedData, bigString, bigBoringString */ + +describe('Encryption and Decryption', function (){ + let context = null; + let good_fpr = inputvalues.encrypt.good.fingerprint; + + before(function (done){ + const prm = Gpgmejs.init({ timeout: 2000 }); + prm.then(function (gpgmejs){ + context = gpgmejs; + done(); + }); + }); + + it('Successful encrypt and decrypt simple string', function (done) { + let data = inputvalues.encrypt.good.data; + context.encrypt({ data: data, publicKeys: good_fpr }).then( + function (answer) { + expect(answer).to.not.be.empty; + expect(answer.data).to.be.a('string'); + expect(answer.data).to.include('BEGIN PGP MESSAGE'); + expect(answer.data).to.include('END PGP MESSAGE'); + context.decrypt({ data: answer.data }).then(function (result) { + expect(result).to.not.be.empty; + expect(result.data).to.be.a('string'); + expect(result.data).to.equal( + inputvalues.encrypt.good.data); + done(); + }); + }); + }); + + it('Decrypt simple non-ascii', function (done) { + let data = encryptedData; + context.decrypt({ data: data }).then(function (result) { + expect(result).to.not.be.empty; + expect(result.data).to.be.a('string'); + expect(result.data).to.equal( + '¡Äußerste µ€ før ñoquis@hóme! Добрый день\n'); + done(); + }); + }).timeout(3000); + + it('Trailing whitespace and different line endings', function (done) { + const data = 'Keks. \rKeks \n Keks \r\n'; + context.encrypt({ data: data, publicKeys: good_fpr }).then( + function (answer) { + expect(answer).to.not.be.empty; + expect(answer.data).to.be.a('string'); + expect(answer.data).to.include('BEGIN PGP MESSAGE'); + expect(answer.data).to.include('END PGP MESSAGE'); + + context.decrypt({ data: answer.data }).then(function (result) { + expect(result).to.not.be.empty; + expect(result.data).to.be.a('string'); + expect(result.data).to.equal(data); + done(); + }); + }); + }).timeout(5000); + + it('Random data, as string', function (done) { + let data = bigString(1000); + context.encrypt({ data:data, publicKeys: good_fpr }).then( + function (answer) { + expect(answer).to.not.be.empty; + expect(answer.data).to.be.a('string'); + expect(answer.data).to.include( + 'BEGIN PGP MESSAGE'); + expect(answer.data).to.include( + 'END PGP MESSAGE'); + context.decrypt({ data: answer.data }).then(function (result) { + expect(result).to.not.be.empty; + expect(result.data).to.be.a('string'); + expect(result.data).to.equal(data); + done(); + }); + }); + }).timeout(3000); + + it('Data, input as base64', function (done) { + let data = inputvalues.encrypt.good.data; + let b64data = btoa(data); + context.encrypt({ data: b64data, publicKeys: good_fpr, base64: true }) + .then(function (answer) { + expect(answer).to.not.be.empty; + expect(answer.data).to.be.a('string'); + expect(answer.data).to.include( + 'BEGIN PGP MESSAGE'); + expect(answer.data).to.include( + 'END PGP MESSAGE'); + context.decrypt({ data: answer.data }).then(function (result) { + expect(result).to.not.be.empty; + expect(result.data).to.be.a('string'); + expect(result.data).to.equal(data); + done(); + }); + }); + }).timeout(3000); + + it('Random data, input as base64', function (done) { + let data = bigBoringString(0.001); + let b64data = btoa(data); + context.encrypt( + { data: b64data, publicKeys: good_fpr, base64: true }) + .then(function (answer) { + expect(answer).to.not.be.empty; + expect(answer.data).to.be.a('string'); + expect(answer.data).to.include( + 'BEGIN PGP MESSAGE'); + expect(answer.data).to.include( + 'END PGP MESSAGE'); + context.decrypt({ data:answer.data }).then( + function (result) { + expect(result).to.not.be.empty; + expect(result.data).to.be.a('string'); + expect(result.data).to.equal(data); + done(); + }); + }); + }).timeout(3000); + + it('Random data, original data is and should stay base64 encoded', + function (done) { + let data = bigBoringString(0.001); + let b64data = btoa(data); + context.encrypt( + { data: b64data, publicKeys: good_fpr }) + .then(function (answer) { + expect(answer).to.not.be.empty; + expect(answer.data).to.be.a('string'); + expect(answer.data).to.include( + 'BEGIN PGP MESSAGE'); + expect(answer.data).to.include( + 'END PGP MESSAGE'); + context.decrypt({ + data:answer.data, expect: 'base64' }) + .then(function (result) { + expect(result).to.not.be.empty; + expect(result.data).to.be.a('string'); + expect(result.data).to.equal(b64data); + done(); + }); + }); + }).timeout(3000); + + for (let j = 0; j < inputvalues.encrypt.good.data_nonascii_32.length; j++){ + it('Roundtrip with >1MB non-ascii input meeting default chunksize (' + + (j + 1) + '/' + + inputvalues.encrypt.good.data_nonascii_32.length + ')', + function (done) { + let input = inputvalues.encrypt.good.data_nonascii_32[j]; + expect(input).to.have.length(32); + let data = ''; + for (let i=0; i < 34 * 1024; i++){ + data += input; + } + context.encrypt({ data: data, publicKeys: good_fpr }) + .then(function (answer) { + expect(answer).to.not.be.empty; + expect(answer.data).to.be.a('string'); + expect(answer.data).to.include( + 'BEGIN PGP MESSAGE'); + expect(answer.data).to.include( + 'END PGP MESSAGE'); + context.decrypt({ data: answer.data }) + .then(function (result) { + expect(result).to.not.be.empty; + expect(result.data).to.be.a('string'); + expect(result.data).to.equal(data); + done(); + }); + }); + }).timeout(5000); + } +}); diff --git a/lang/js/BrowserTestExtension/tests/encryptTest.js b/lang/js/BrowserTestExtension/tests/encryptTest.js new file mode 100644 index 0000000..1017d71 --- /dev/null +++ b/lang/js/BrowserTestExtension/tests/encryptTest.js @@ -0,0 +1,159 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +/* global describe, it, expect, before, Gpgmejs */ +/* global inputvalues, fixedLengthString, bigString */ + +describe('Encryption', function () { + let context = null; + const good_fpr = inputvalues.encrypt.good.fingerprint; + before(function (done){ + const prm = Gpgmejs.init({ timeout: 2000 }); + prm.then(function (gpgmejs){ + context = gpgmejs; + done(); + }); + }); + + it('Successful encrypt', function (done) { + const data = inputvalues.encrypt.good.data; + context.encrypt({ data: data, publicKeys: good_fpr }) + .then(function (answer) { + expect(answer).to.not.be.empty; + expect(answer.data).to.be.a('string'); + expect(answer.data).to.include('BEGIN PGP MESSAGE'); + expect(answer.data).to.include('END PGP MESSAGE'); + done(); + }); + }); + + + it( 'encrypt with \'armor\': true should returned an armored block', + function (done){ + const data = bigString(1000); + context.encrypt({ data: data, publicKeys: good_fpr, armor: true }) + .then(function (answer){ + expect(answer).to.not.be.empty; + expect(answer.data).to.be.a('string'); + expect(answer.data).to.include('BEGIN PGP MESSAGE'); + expect(answer.data).to.include('END PGP MESSAGE'); + expect(answer.format).to.equal('ascii'); + done(); + }); + }); + + it( 'encrypt with \'armor\': false and \'expected\': \'uint8\' returns ' + + 'an Uint8Array', function (done) { + const data = bigString(1000); + context.encrypt({ + data: data, publicKeys: good_fpr, armor: false, expect: 'uint8' + }).then(function (answer){ + expect(answer).to.not.be.empty; + expect(answer.data).to.be.a('Uint8Array'); + expect(answer.format).to.equal('uint8'); + done(); + }); + }); + + it( 'encrypt with \'armor\': false and \'expected\': \'base64\' returns ' + + 'a base64 string', function (done) { + const data = bigString(1000); + context.encrypt({ + data: data, publicKeys: good_fpr, armor: false, expect: 'base64' + }).then(function (answer){ + expect(answer).to.not.be.empty; + expect(answer.data).to.be.a('string'); + expect(answer.format).to.equal('base64'); + done(); + }); + }); + + const sizes = [5,20,50]; + for (let i=0; i < sizes.length; i++) { + it('Successful encrypt a ' + sizes[i] + 'MB message', function (done) { + const data = fixedLengthString(sizes[i]); + context.encrypt({ data: data, publicKeys: good_fpr }) + .then(function (answer) { + expect(answer).to.not.be.empty; + expect(answer.data).to.be.a('string'); + expect(answer.data).to.include('BEGIN PGP MESSAGE'); + expect(answer.data).to.include('END PGP MESSAGE'); + done(); + }); + }).timeout(20000); + } + + it('Sending encryption without keys fails', function (done) { + const data = inputvalues.encrypt.good.data; + context.encrypt({ data: data }).then(function (answer) { + expect(answer).to.be.undefined; + }, function (error){ + expect(error).to.be.an('Error'); + expect(error.code).to.equal('MSG_INCOMPLETE'); + done(); + }); + }); + + it('Sending encryption without data fails', function (done) { + context.encrypt({ data: null, publicKeys: good_fpr }) + .then(function (answer) { + expect(answer).to.be.undefined; + }, function (error) { + expect(error).to.be.an.instanceof(Error); + expect(error.code).to.equal('MSG_INCOMPLETE'); + done(); + }); + }); + + it('Sending encryption with non existing keys fails', function (done) { + const data = inputvalues.encrypt.good.data; + const bad_fpr = inputvalues.encrypt.bad.fingerprint; + context.encrypt({ data:data, publicKeys: bad_fpr }) + .then(function (answer) { + expect(answer).to.be.undefined; + }, function (error){ + expect(error).to.be.an('Error'); + expect(error.code).to.not.be.undefined; + expect(error.code).to.equal('GNUPG_ERROR'); + done(); + }); + }).timeout(5000); + + it('Overly large message ( > 64MB) is rejected', function (done) { + const data = fixedLengthString(65); + context.encrypt({ data: data, publicKeys: good_fpr }) + .then(function (answer) { + expect(answer).to.be.undefined; + }, function (error){ + expect(error).to.be.an.instanceof(Error); + // TODO: there is a 64 MB hard limit at least in chrome at: + // chromium//extensions/renderer/messaging_util.cc: + // kMaxMessageLength + // The error will be a browser error, not from gnupg or from + // this library + done(); + }); + }).timeout(8000); + + +}); diff --git a/lang/js/BrowserTestExtension/tests/inputvalues.js b/lang/js/BrowserTestExtension/tests/inputvalues.js new file mode 100644 index 0000000..730e48a --- /dev/null +++ b/lang/js/BrowserTestExtension/tests/inputvalues.js @@ -0,0 +1,453 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +const inputvalues = {// eslint-disable-line no-unused-vars + encrypt: { + good:{ + data : 'Hello World.', + // Fingerprint of a key that has been imported to gnupg + // (i.e. see testkey.pub; testkey.sec) + fingerprint : 'D41735B91236FDB882048C5A2301635EEFF0CB05', + fingerprint_mixedcase: 'D41735B91236fdb882048C5A2301635eeFF0Cb05', + data_nonascii: '¡Äußerste µ€ før ñoquis@hóme! Добрый день', + + // used for checking encoding consistency in > 2MB messages. + data_nonascii_32: [ + 'K€K€K€K€K€K€K€K€K€K€K€K€K€K€K€K€', + 'µ€µ€µ€µ€µ€µ€µ€µ€µ€µ€µ€µ€µ€µ€µ€µ€', + '€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€', + '²³²³²³²³²³²³²³²³²³²³²³²³²³²³²³²³', + 'µ€µ€µ€µ€µ€µ€µ€µ€µ€µ€µ€A€µ€µ€µ€µ€', + 'µ€µ€µ€µ€µ€µ€µ€µ€µ€µ€µ€µAµ€µ€µ€µ€', + 'üüüüüüüüüüüüüüüüüüüüüüüüüüüüüüüü', + 'µAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA€', + 'µAAAAµAAAAAAAAAAAAAAAAAAAAAAAAA€', + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAµ€', + 'µAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA°', + '€AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA€', + 'µ||||||||||||||||||||||||||||||€', + 'æſæſ³¼„¬“³³¬“¬½”æſæſ³¼„¬“³³¬“¬½”' + ] + }, + bad: { + // valid Hex value, but not usable (not imported to gnupg, or + // bogus fingerprint) + fingerprint: 'CDC3A2B2860625CCBFC5AAAAAC6D1B604967FC4A' + } + }, + + signedMessage: { + good: '-----BEGIN PGP SIGNED MESSAGE-----\n' + + 'Hash: SHA256\n' + + '\n' + + 'Matschige Münsteraner Marshmallows\n' + + '-----BEGIN PGP SIGNATURE-----\n' + + '\n' + + 'iQEzBAEBCAAdFiEE1Bc1uRI2/biCBIxaIwFjXu/wywUFAltRoiMACgkQIwFjXu/w\n' + + 'ywUvagf6ApQbZbTPOROqfTfxAPdtzJsSDKHla6D0G5wom2gJbAVb0B2YS1c3Gjpq\n' + + 'I4kTKT1W1RRkne0mK9cexf4sjb5DQcV8PLhfmmAJEpljDFei6i/E309BvW4CZ4rG\n' + + 'jiurf8CkaNkrwn2fXJDaT4taVCX3V5FQAlgLxgOrm1zjiGA4mz98gi5zL4hvZXF9\n' + + 'dHY0jLwtQMVUO99q+5XC1TJfPsnteWL9m4e/YYPfYJMZZso+/0ib/yX5vHCk7RXH\n' + + 'CfhY40nMXSYdfl8mDOhvnKcCvy8qxetFv9uCX06OqepAamu/bvxslrzocRyJ/eq0\n' + + 'T2JfzEN+E7Y3PB8UwLgp/ZRmG8zRrQ==\n' + + '=ioB6\n' + + '-----END PGP SIGNATURE-----\n', + bad: '-----BEGIN PGP SIGNED MESSAGE-----\n' + + 'Hash: SHA256\n' + + '\n' + + 'Matschige Münchener Marshmallows\n' + + '-----BEGIN PGP SIGNATURE-----\n' + + '\n' + + 'iQEzBAEBCAAdFiEE1Bc1uRI2/biCBIxaIwFjXu/wywUFAltRoiMACgkQIwFjXu/w\n' + + 'ywUvagf6ApQbZbTPOROqfTfxAPdtzJsSDKHla6D0G5wom2gJbAVb0B2YS1c3Gjpq\n' + + 'I4kTKT1W1RRkne0mK9cexf4sjb5DQcV8PLhfmmAJEpljDFei6i/E309BvW4CZ4rG\n' + + 'jiurf8CkaNkrwn2fXJDaT4taVCX3V5FQAlgLxgOrm1zjiGA4mz98gi5zL4hvZXF9\n' + + 'dHY0jLwtQMVUO99q+5XC1TJfPsnteWL9m4e/YYPfYJMZZso+/0ib/yX5vHCk7RXH\n' + + 'CfhY40nMXSYdfl8mDOhvnKcCvy8qxetFv9uCX06OqepAamu/bvxslrzocRyJ/eq0\n' + + 'T2JfzEN+E7Y3PB8UwLgp/ZRmG8zRrQ==\n' + + '=ioB6\n' + + '-----END PGP SIGNATURE-----\n' + }, + encryptSignedMessage: '-----BEGIN PGP MESSAGE-----\n'+ + '\n'+ + 'hQEMA6B8jfIUScGEAQf/bmQ+xNMGTjPvQCktkxR4Svt2dVNVdSzKsCmvSv24QOQF\n'+ + 'yBMK5w51S/6DTdiZI12IYD7hjvkr9NqxXXupjrVKwqEVpg4Pkwckac0OcElJIBsL\n'+ + '3htr4iYsr8dhSgSS4BO0azcu4wZQTXy5v2P7yYPECMEagNEXnW+tE7sHLCq8Ysqz\n'+ + 'LVxG0R0IUijKeEd3xQC2Tt20e1Z1j5tnqaPhE/9Smqf5OjSUDqpXxvRnSNRk/zEs\n'+ + 'cGVgCF+cv68nUJM9lwEAbBQChplwL6ebnhunC6DsRCxnjLHVyKm127hmhSiMGC0e\n'+ + 'Ns31mGeP1dxpDv6Gi2/oKmq67vG3i4fKeckj7bt30tLA1wH0Qn5Mn6Tzxzve0W0q\n'+ + 'Ghqn9PY9qNK8EkrkzqaFk9dzu5tfSbaJBLS/uIhX2Wj70EMEBbFSkN0qlgOfLgGw\n'+ + '5mwRvCgj4nvV1ByFhnx7uwgQixvOwLH4JLKvwCQpJm+O2R0eC7M6CzR/b9iL/oaO\n'+ + 'JTkoD9hcLhxF7j+3ZYg7rbNwofuHST097vFjzItsucb0jHOzjlkCqbhdczICILTa\n'+ + 'H76Q6YGdMLyG9a3s4yZUMruaeQyWGeXlryzLDvdEoSgoD5YrolsFOM+Z2apbzVs2\n'+ + 'k5CltwtanjjWGnpAqSyr49C6CSU8G1QHpNygx5frtAS8bojR2ovB9OJp2wUklDvC\n'+ + 'LtU7dLpTY/BIvfB1vzwcW/aNgmPadNHX8mAzlqTQJjeLoo69Wp804t+u36sgfd/J\n'+ + 'ser7vdJJUm+86Q9csefItvFmHhqjMg5XXHoa8WZWJOHIQMxZkaIwKAzcEt/oEOdJ\n'+ + 'rbVNVabhTdbmS5I1ok16wg5jMF07ZDM7nXWMcQNjwT646XKP+pp2N6YQROVidNXj\n'+ + 'COyRyiXE/csr\n'+ + '=Ik7G\n'+ + '-----END PGP MESSAGE-----\n', + someInputParameter: 'bad string', + + publicKeyNonAscii: { + userid: 'Müller €uro', + key: '-----BEGIN PGP PUBLIC KEY BLOCK-----\n' + '\n' + + 'mQENBFt2/VIBCADIWBIMxExZlHda4XIVnM9nsIfUYLebJSC/krEriyWgzytU8/fQ\n' + + 'S05cfnYx7RXvOOq4k8aa7mu80ovg3q77idXauLreAUwng4Njw0nMxWq/vtoMiZ60\n' + + '9f8EmfthZophhkQF2HIPHyqXMDZzMLWv4oTr2UJ9BKudL1XtbK51y9TbiyfQygBl\n' + + '8bl+zrOo70/dN6aunvuo6Hlu5cEzkj2QrzZlqTdfG5qv6KVEMut1eAbxZAmvSnna\n' + + 'R4wqiRCT3/eRXGJbDL/8GaCEYkwi9FBrimjOTV0MpcLNwAU4aGfDxMUsxML9xJ+/\n' + + '/6GFxzYf7Lmk5UhvoewR58uQkHkTVPjZ9hXZABEBAAG0KE3DvGxsZXIg4oKsdXJv\n' + + 'IDxtdWVsbGVyZXVyb0BleGFtcGxlLm9yZz6JAVQEEwEIAD4WIQQVNixp3XT/DuGT\n' + + 'F4MFmkL4L5UZdAUCW3b9UgIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIX\n' + + 'gAAKCRAFmkL4L5UZdAhiCACowW1aC8DYGtJyAaBO2MqWhyw1wVCbQN9uFsQZPydY\n' + + 'v3BEbCDrRc0HyfV1PVoRQsgkiNMes1S2tz2IMJoEOTMaz3WjPM8yK0dDbo5sfx/o\n' + + '/XaXeKhyYNqRkz2dPzorg1sHyHe0ki/HoQiANEJ8mByMtlwnPWlhnINAX+27eL17\n' + + 'JC8juhBYUchqoIBAl+ajYKSThdLzrUkcL7QfJjZb3pPytJSTTdFc0rD6ERDbfXXc\n' + + '/vnE2SDYme+XXn7H5tNe67tPM8M96vbp+uM+n2t/z96C+Pqb6GJFMBa35PM+/qQO\n' + + 'yr0I2oaQnTecx2AfBXGZvd81wMYikAJ9rAOWyMQZHJWouQENBFt2/VIBCADXCvKD\n' + + '3wRWCOzRWtLTs7hpAjCDxp6niPkwxKuUf9r/sUPmn0pWdZHYlbPDev9psN9bnJ+C\n' + + '+wzzPZ1zgSYKIAN0IMoh0L7BRAoau7VWQ3Q7hP6HIbdzOTEGyklSoh9pIh6IlwZZ\n' + + 'XfPlFlnn7FeH1UeA711E174SUpDRKYSfT+mFObQUuQewGi9QC3gBsz5MPLQQLzML\n' + + 'yimIOT+8i64fHHSKChw5ZDckBffej31/YHPQ7+JsWFV+G/6xDfbwnaFZFAUwo+1L\n' + + '4w9UiMyCNkIWCkulzJ2Hbz66xzFMi/8zMYxr08Af+PpsXaWTQHAa5V4GNJSInDEB\n' + + '7gy/CGLcY90EozoDABEBAAGJATwEGAEIACYWIQQVNixp3XT/DuGTF4MFmkL4L5UZ\n' + + 'dAUCW3b9UgIbDAUJA8JnAAAKCRAFmkL4L5UZdPqoB/9kpqxqa82k7JMcq7UiwQY7\n' + + 'CdqCUPKF88ciOWKBpZmpl8V7zgM7kEXwmM6ocHcznXi8xM7eOfDIJcBeqFVIE4OT\n' + + '63OCMuvZICM9Kiu48wLNAw5W/YGAOBH7ySQzZM2XrtvwfFtJ3lR00t5f4FVtriA5\n' + + '47BjYYG5tTdJc8HwEHs045S99xKCWqwuDgO9qskIi6iPePUkuhpaVBLuEj2Goku6\n' + + 'i8aql/vKYQS67L7UHJiEbjLe+wP9k3FvWUFTx39lAubsDzb4Abhe+qRqs2TKD7Go\n' + + 'k35ZriRIYllmx4c9KyWL7Mvzcp+84Sq9LeMfsN4JstBDJ7jn6g19SjO5dmtxSuP0\n' + + '=zZSJ\n' + + '-----END PGP PUBLIC KEY BLOCK-----\n' + } +}; + +// (Pseudo-)Random String covering all of utf8. +function bigString (length){// eslint-disable-line no-unused-vars + let arr = []; + for (let i= 0; i < length; i++){ + arr.push(String.fromCharCode( + Math.floor(Math.random() * 10174) + 1) + ); + } + return arr.join(''); +} + +function fixedLengthString (megabytes){// eslint-disable-line no-unused-vars + let maxlength = 1024 * 1024 * megabytes / 2; + let uint = new Uint8Array(maxlength); + for (let i = 0; i < maxlength; i++){ + uint[i] = Math.floor(Math.random()* 256); + } + let td = new TextDecoder('ascii'); + let result = td.decode(uint); + return result; +} + +// (Pseudo-)Random Uint8Array, given size in Megabytes +function bigUint8 (megabytes){// eslint-disable-line no-unused-vars + let maxlength = 1024 * 1024 * megabytes; + let uint = new Uint8Array(maxlength); + for (let i= 0; i < maxlength; i++){ + uint[i] = Math.floor(Math.random() * 256); + } + return uint; +} + +// (Pseudo-)Random string with very limited charset +// (ascii only, no control chars) +function bigBoringString (megabytes){// eslint-disable-line no-unused-vars + let maxlength = 1024 * 1024 * megabytes; + let string = []; + let chars = + ' 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + for (let i= 0; i < maxlength; i++){ + string.push(chars[Math.floor(Math.random() * chars.length)]); + } + return string.join(''); +} + +// Some String with simple chars, with different characteristics, but still +// expected to occur in an averag message +// eslint-disable-next-line no-unused-vars +function slightlyLessBoringString (megabytes, set){ + let maxlength = 1024 * 1024 * megabytes; + let string = []; + let chars = ''; + if (set ===1 ) { + chars = '\n"\r \''; + } else if (set === 2 ) { + chars = '()=?`#+-{}[]'; + } else if (set === 3){ + chars = '^°/'; + } else if (set ===4) { + chars = 'äüßµüþÖ~ɁÑ||@'; + } else { + chars = '*<>\n"\r§$%&/()=?`#+-{}[] \''; + } + for (let i= 0; i < maxlength; i++){ + string.push(chars[Math.floor(Math.random() * chars.length)]); + } + return string.join(''); +} + +// Data encrypted with testKey +const encryptedData =// eslint-disable-line no-unused-vars + '-----BEGIN PGP MESSAGE-----\n' + + '\n' + + 'hQEMA6B8jfIUScGEAQgAlANd3uyhmhYLzVcfz4LEqA8tgUC3n719YH0iuKEzG/dv\n' + + 'B8fsIK2HoeQh2T3/Cc2LBMjgn4K33ksG3k2MqrbIvxWGUQlOAuggc259hquWtX9B\n' + + 'EcEoOAeh5DuZT/b8CM5seJKNEpPzNxbEDiGikp9DV9gfIQTTUnrDjAu5YtgCN9vA\n' + + '3PJxihioH8ODoQw2jlYSkqgXpBVP2Fbx7qgTuxGNu5w36E0/P93//4hDXcKou7ez\n' + + 'o0+NEGSkbaY+OPk1k7k9n+vBSC3F440dxsTNs5WmRvx9XZEotJkUBweE+8XaoLCn\n' + + '3RrtyD/lj63qi3dbyI5XFLuPU1baFskJ4UAmI4wNhdJ+ASailpnFBnNgiFBh3ZfB\n' + + 'G5Rmd3ocSL7l6lq1bVK9advXb7vcne502W1ldAfHgTdQgc2CueIDFUYAaXP2OvhP\n' + + 'isGL7jOlDCBKwep67ted0cTRPLWkk3NSuLIlvD5xs6L4z3rPu92gXYgbZoMMdP0N\n' + + 'kSAQYOHplfA7YJWkrlRm\n' + + '=zap6\n' + + '-----END PGP MESSAGE-----\n'; + +const ImportablePublicKey = {// eslint-disable-line no-unused-vars + fingerprint: '78034948BA7F5D0E9BDB67E4F63790C11E60278A', + key:'-----BEGIN PGP PUBLIC KEY BLOCK-----\n' + + '\n' + + 'mQENBFsPvK0BCACaIgoIN+3g05mrTITULK/YDTrfg4W7RdzIZBxch5CM0zdu/dby\n' + + 'esFwaJbVQIqu54CRz5xKAiWmRrQCaRvhvjY0na5r5UUIpbeQiOVrl65JtNbRmlik\n' + + 'd9Prn1kZDUOZiCPIKn+/M2ecJ92YedM7I4/BbpiaFB11cVrPFg4thepn0LB3+Whp\n' + + '9HDm4orH9rjy6IUr6yjWNIr+LYRY6/Ip2vWcMVjleEpTFznXrm83hrJ0n0INtyox\n' + + 'Nass4eDWkgo6ItxDFFLOORSmpfrToxZymSosWqgux/qG6sxHvLqlqy6Xe3ZYRFbG\n' + + '+JcA1oGdwOg/c0ndr6BYYiXTh8+uUJfEoZvzABEBAAG0HEJsYSBCbGEgPGJsYWJs\n' + + 'YUBleGFtcGxlLm9yZz6JAVQEEwEIAD4WIQR4A0lIun9dDpvbZ+T2N5DBHmAnigUC\n' + + 'Ww+8rQIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD2N5DBHmAn\n' + + 'igwIB/9K3E3Yev9taZP4KnXPhk1oMQRW1MWAsFGUr+70N85VwedpUawymW4vXi1+\n' + + 'hMeTc39QjmZ0+VqHkJttkqEN6bLcEvgmU/mOlOgKdzy6eUcasYAzgoAKUqSX1SPs\n' + + '0Imo7Tj04wnfnVwvKxaeadi0VmdqIYaW75UlrzIaltsBctyeYH8sBrvaTLscb4ON\n' + + '46OM3Yw2G9+dBF0P+4UYFHP3EYZMlzNxfwF+i2HsYcNDHlcLfjENr9GwKn5FJqpY\n' + + 'Iq3qmI37w1hVasHDxXdz1X06dpsa6Im4ACk6LXa7xIQlXxTgPAQV0sz2yB5eY+Md\n' + + 'uzEXPGW+sq0WRp3hynn7kVP6QQYvuQENBFsPvK0BCACwvBcmbnGJk8XhEBRu2QN3\n' + + 'jKgVs3CG5nE2Xh20JipZwAuGHugDLv6/jlizzz5jtj3SAHVtJB8lJW8I0cNSEIX8\n' + + 'bRYH4C7lP2DTb9CgMcGErQIyK480+HIsbsZhJSNHdjUUl6IPEEVfSQzWaufmuswe\n' + + 'e+giqHiTsaiW20ytXilwVGpjlHBaxn/bpskZ0YRasgnPqKgJD3d5kunNqWoyCpMc\n' + + 'FYgDERvPbhhceFbvFE9G/u3gbcuV15mx53dDX0ImvPcvJnDOyJS9yr7ApdOV312p\n' + + 'A1MLbxfPnbnVu+dGXn7D/VCDd5aBYVPm+5ANrk6z9lYKH9aO5wgXpLAdJvutCOL5\n' + + 'ABEBAAGJATwEGAEIACYWIQR4A0lIun9dDpvbZ+T2N5DBHmAnigUCWw+8rQIbDAUJ\n' + + 'A8JnAAAKCRD2N5DBHmAnigMVB/484G2+3R0cAaj3V/z4gW3MRSMhcYqEMyJ/ACdo\n' + + '7y8eoreYW843JWWVDRY6/YcYYGuBBP47WO4JuP2wIlVn17XOCSgnNjmmjsIYiAzk\n' + + 'op772TB27o0VeiFX5iWcawy0EI7JCb23xpI+QP31ksL2yyRYFXCtXSUfcOrLpCY8\n' + + 'aEQMQbAGtkag1wHTo/Tf/Vip8q0ZEQ4xOKTR2/ll6+inP8kzGyzadElUnH1Q1OUX\n' + + 'd2Lj/7BpBHE2++hAjBQRgnyaONF7mpUNEuw64iBNs0Ce6Ki4RV2+EBLnFubnFNRx\n' + + 'fFJcYXcijhuf3YCdWzqYmPpU/CtF4TgDlfSsdxHxVOmnZkY3\n' + + '=qP6s\n' + + '-----END PGP PUBLIC KEY BLOCK-----\n', + + keyChangedUserId: '-----BEGIN PGP PUBLIC KEY BLOCK-----\n' + + '\n' + + 'mQENBFsPvK0BCACaIgoIN+3g05mrTITULK/YDTrfg4W7RdzIZBxch5CM0zdu/dby\n' + + 'esFwaJbVQIqu54CRz5xKAiWmRrQCaRvhvjY0na5r5UUIpbeQiOVrl65JtNbRmlik\n' + + 'd9Prn1kZDUOZiCPIKn+/M2ecJ92YedM7I4/BbpiaFB11cVrPFg4thepn0LB3+Whp\n' + + '9HDm4orH9rjy6IUr6yjWNIr+LYRY6/Ip2vWcMVjleEpTFznXrm83hrJ0n0INtyox\n' + + 'Nass4eDWkgo6ItxDFFLOORSmpfrToxZymSosWqgux/qG6sxHvLqlqy6Xe3ZYRFbG\n' + + '+JcA1oGdwOg/c0ndr6BYYiXTh8+uUJfEoZvzABEBAAG0HEJsYSBCbGEgPGJsYWJs\n' + + 'YUBleGFtcGxlLm9yZz6JAVQEEwEIAD4WIQR4A0lIun9dDpvbZ+T2N5DBHmAnigUC\n' + + 'Ww+8rQIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD2N5DBHmAn\n' + + 'igwIB/9K3E3Yev9taZP4KnXPhk1oMQRW1MWAsFGUr+70N85VwedpUawymW4vXi1+\n' + + 'hMeTc39QjmZ0+VqHkJttkqEN6bLcEvgmU/mOlOgKdzy6eUcasYAzgoAKUqSX1SPs\n' + + '0Imo7Tj04wnfnVwvKxaeadi0VmdqIYaW75UlrzIaltsBctyeYH8sBrvaTLscb4ON\n' + + '46OM3Yw2G9+dBF0P+4UYFHP3EYZMlzNxfwF+i2HsYcNDHlcLfjENr9GwKn5FJqpY\n' + + 'Iq3qmI37w1hVasHDxXdz1X06dpsa6Im4ACk6LXa7xIQlXxTgPAQV0sz2yB5eY+Md\n' + + 'uzEXPGW+sq0WRp3hynn7kVP6QQYvtCZTb21lb25lIEVsc2UgPHNvbWVvbmVlbHNl\n' + + 'QGV4YW1wbGUub3JnPokBVAQTAQgAPhYhBHgDSUi6f10Om9tn5PY3kMEeYCeKBQJb\n' + + 'D705AhsDBQkDwmcABQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEPY3kMEeYCeK\n' + + 'aIUH/2o+Ra+GzxgZrVexXLL+FCSmcu0cxeWfMhL8jd96c6uXIT21qQMRU2jgvnUp\n' + + 'Wdi/BeLKp5lYwywm04PFhmRVxWXLuLArCsDu+CFys+aPeybnjikPBZov6P8/cZV3\n' + + 'cd6zxFvqB9J15HjDMcl/r5v6d4CgSLKlFebrO5WKxHa6zGK9TRMQrqTu1heKHRf6\n' + + '4+Wj+MZmYnPzEQePjiBw/VkJ1Nm37Dd24gKdcN/qJFwEOqvbI5RIjB7xqoDslZk9\n' + + 'sAivBXwF0E9HKqvh4WZZeA7uaWNdGo/cQkD5rab5SdHGNPHLbzoRWScsM8WYtsME\n' + + 'dEMp5iPuG9M63+TD7losAkJ/TlS5AQ0EWw+8rQEIALC8FyZucYmTxeEQFG7ZA3eM\n' + + 'qBWzcIbmcTZeHbQmKlnAC4Ye6AMu/r+OWLPPPmO2PdIAdW0kHyUlbwjRw1IQhfxt\n' + + 'FgfgLuU/YNNv0KAxwYStAjIrjzT4cixuxmElI0d2NRSXog8QRV9JDNZq5+a6zB57\n' + + '6CKoeJOxqJbbTK1eKXBUamOUcFrGf9umyRnRhFqyCc+oqAkPd3mS6c2pajIKkxwV\n' + + 'iAMRG89uGFx4Vu8UT0b+7eBty5XXmbHnd0NfQia89y8mcM7IlL3KvsCl05XfXakD\n' + + 'UwtvF8+dudW750ZefsP9UIN3loFhU+b7kA2uTrP2Vgof1o7nCBeksB0m+60I4vkA\n' + + 'EQEAAYkBPAQYAQgAJhYhBHgDSUi6f10Om9tn5PY3kMEeYCeKBQJbD7ytAhsMBQkD\n' + + 'wmcAAAoJEPY3kMEeYCeKAxUH/jzgbb7dHRwBqPdX/PiBbcxFIyFxioQzIn8AJ2jv\n' + + 'Lx6it5hbzjclZZUNFjr9hxhga4EE/jtY7gm4/bAiVWfXtc4JKCc2OaaOwhiIDOSi\n' + + 'nvvZMHbujRV6IVfmJZxrDLQQjskJvbfGkj5A/fWSwvbLJFgVcK1dJR9w6sukJjxo\n' + + 'RAxBsAa2RqDXAdOj9N/9WKnyrRkRDjE4pNHb+WXr6Kc/yTMbLNp0SVScfVDU5Rd3\n' + + 'YuP/sGkEcTb76ECMFBGCfJo40XualQ0S7DriIE2zQJ7oqLhFXb4QEucW5ucU1HF8\n' + + 'UlxhdyKOG5/dgJ1bOpiY+lT8K0XhOAOV9Kx3EfFU6admRjc=\n' + + '=9WZ7\n' + + '-----END PGP PUBLIC KEY BLOCK-----\n' +}; + +/** + * Changes base64 encoded gpg messages + * @param {String} msg input message + * @param {Number} rate of changes as percentage of message length. + * @param {[Number, Number]} p begin and end of the message left untouched (to + * preserve) header/footer + */ +// eslint-disable-next-line no-unused-vars +function sabotageMsg (msg, rate = 0.01, p= [35,35]){ + const iterations = Math.floor(Math.random() * msg.length * rate) + 1; + const base64_set = + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/'; + for (let i=0; i < iterations; i++){ + let str0, str1, str2; + const chosePosition = function (){ + let position = + Math.floor( Math.random() * (msg.length - p[0] + p[1])) + + p[0]; + str1 = msg.substring(position,position+1); + if (str1 === '\n'){ + chosePosition(); + } else { + str0 = msg.substring(0,position); + str2 = msg.substring(position +1); + } + }; + chosePosition(); + let new1 = function (){ + let n = base64_set[Math.floor(Math.random() * 64)]; + return (n === str1) ? new1() : n; + }; + msg = str0.concat(new1()).concat(str2); + } + return msg; +} + +// a simple png +// eslint-disable-next-line no-unused-vars +const binaryData = { + base64: + 'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAIAAABvFaqvAAAEwElEQVQ4y62R228b1xn' + + 'E51z27I271K64okTqRkuOLRuNHcNBCqTwS//qvvYCtAmQNrETO7akyJJsipRIUeSSez' + + 'vnfH1IkBboW5F5mpf5ATPDiAi/hTh+I8n/I/NrCcbY/4DI/GKY+E/CGLIWROCccQ7Or' + + 'bVVXWmttTHKUY6UjuP8jJMggilhVjAluILwIUMwRlpTUVBVQWsoxT1PS3E7v7u6Ho0m' + + '48boOIz63V6WdsIgZIxJmBXyMyx/QjWEbCPYReuQRNsWhbm5tqMRFQVPU6x3JlZ/e/r' + + 'm76+/OR6elbrKovT5/ae/f/L5YGcQeL5EOcHsa8z/hOY1eIbVF6b5o/YeNrnWFxf2+5' + + 'fs5kY9PDKHdlgu/vrPv/357F95UxLo7G44nk+EEO2orbKuRHOL1feo/wJ8hFEmn5dNv' + + 'JB8MRP1xYV79j68HrEss1ub15Ph8dXpoil+3lqTPZ+PXp7+8On9x51kXcLUsAtgARBR' + + 'ZfRpnn93voouxsJM5ptJsp1lfn+HwtDOpAX77/sM2bvVfFUW2hgJJ4bTQ9UH5YA1psz' + + 'zyYery69ezaihzx883BgM2GDgxFGqq92N3YvltDA1CGDwJU8j3/UYZ1rCTRE+QfMBpQ' + + 'BdgRzwAFwyKSAV1jvU38HGptvy+4J/8ej5sih+uHq31HUg+ePexvNHg27mcGch4aZY+' + + '4wsB23Z4j1XCNeOtr0dG6Eyst3tFV47ZwpQcZQcDR6YUnfD9UWxSBL/06Peo0+67WRC' + + 'cinBBKmMnE8s98gcQjlh1Nn2sqArxiusyP1Yu8WdyUzVYhVpkiQ2/PX7W4PdQXdvP1x' + + 'Lx0x9Z8BkXVVU1rQgGjZ2VPNOy9nrxPGmjdRkUs8XuqhEAcOIrCzG1zdv3/x4fn7+2b' + + 'OngTMIPZfL0mJIsHJyfc3LUlyN5Jt37MfX4uixs96TmcO5MpxqggWKhlaNiUBE1pC2M' + + 'BaWOOPM40gY7hEq+fLVKx/olOVa07hEgjEwzhnzlYg9GXs2r21L8dgXLddGaWtwtBv1' + + '/LgbWE9rzn2khK7FWPT7/Xy5bIVBO03U5qbc2+fdDZkkJBUDlGCh4h2fd0LpKlOw6VQ' + + 'MF+FQxwvlyziMAyXBJxbH8vjkhKw92N8peplyOcWRE7mMk6t4J1KRJ1arQsIwW1qqKs' + + 'ymwekV+wcTTKLeNt0OdRwowJEvXrxoJ+1wM8kTNncWQpVtIVLWapEbKIebmvQyv72pl' + + '3da2VJMSzOu1RWAnG7KpjC1VOhK/lR++YcvucPIa6biw6V+zSy7J5/6uh3IlrEMpilm' + + 'N9fvXs0uT4KtnuoHvfBBg4Y46ztHbRvLleSrFpQrDw4OGqpv9Xi0qqf1R1hs6oPaVNp' + + 'oznlT1+VycXd5cvv2a6JnycaTA/93mRqAsYytp0UoRzO7mLPQlVEUNaY2dZ3RVoVnAN' + + 'ZExhpRmrJmDYyRXtDePgDQ3r63nuwEaUaKM8CtyZlN6fhEv33DuxtScAHmhIh62I+cx' + + 'BjDaye/XU3zO85E6HvK9bODo7X+nhdGYdrx/VgoF9ZCL20zqW9v6f17ayEBCCYDGTpc' + + 'Rc5aXdez5d3Hy/PT41MAh4eH+7s7ydY2Z8xRSjiOVC5jDLDkuBRFYm8fAEvSfwPxgHl' + + 'kzr8e4QAAAABJRU5ErkJggg==', + encryptedArmored: '-----BEGIN PGP MESSAGE-----\n' + + '\n' + + 'hQEMA6B8jfIUScGEAQgA3m9gtJswzXITlX3yJslszQSBBtHb3jTquF6ZoB5NPxMC\n' + + '5sX1WkjemyjYOy/pscLb8JRedj+owfaHYAGed1h+SM8iVHrHSVImbq+okzKbkYTB\n' + + 'lYoBK63y2WHjMdMEjHHWl5CPfyzmAutwix0sWGcDLwNdO31eUdKHtjPi69k+IEw2\n' + + 'hJ0+hdulttJk5jvNtW0mmuWjzKPgekZX7AFZnO+cJEgdDXEZ9oOD3/HxM8Rw/Kba\n' + + 't7LWW/h3WxTzUKWrCO7SvcX27MGt94I1ff/2IAPb3/ZiskGrEwN6TYv8x0OaC6Xy\n' + + 'TFxVCSGbXo+dSBFUfMMnPCiY6W8DOrchwDK2sBoGO9LqAUkpzm0tYSYt5v8/vXvO\n' + + 'uXG+u8lG9I/TqnVDdZXXedBdDVFxv03K35FQU48K+AuBJLakdMfCfK3AvYRBgffu\n' + + 'NbdnQUTs8AUM3qNgN95JRM/edhir0Tfz981R8dTYbxRwuosR//yNxCXA1UUg0UeD\n' + + 'feC+AB6lRNt/P2qpt4pio4fflcnTqL5lFpAcktdvX6scKxe/GFR+ylafZMykm2A/\n' + + '+UMfTTjZKm6MqsTnk2fOaHM4AIZBebx7j4cYqV+vyaxla4drZ+7w4P4XveCIS8eD\n' + + 'VkIGRhJuRtXMFTWrV4z8BIDhUw68/h0rClC5mucdEJMUPaEK65mECxMTEggomz5/\n' + + 'BaSVDmNUoLHwqPV1fCUG+FtDhNHtqLDszlhEGNDa2NU4paGRaxJzS3IZExlhbxFX\n' + + 'pNOWS1W/gEblkzslWbOq3Gg95/MjJL8tEhlAuGnlFQqa2ZZ9/QgAujSaSnY5bQFe\n' + + '+riqxkex8YA+2v5yfhwVBF1W4TxvkP/9tL4xw/tepibwZrAEJLih+Cus6X+OC/Qw\n' + + 'qUe2ROmHwoM83iJdsrnoGaenVCBWBzSsTjVauRWD7lPT1Hgwavgz9J35mKjfyayx\n' + + 'XJxbFnHgvsRPOE5usYEsWQYrPQoDin9lpaXq5d+D3QFi/weQSVvKcC6a7jjNxeYS\n' + + 'KCc9lac+qBCLXg8F9Ff2Szkr7XuamMvHbd2FAQFiTQ5zFcrvOL8V8VuhyUERFhJE\n' + + '4xCFq/vwhC3v7+aRWhpBfRvb5IE45fHTCZsvXt8U4YdzaL/OiDzv+/S0xHda6cJx\n' + + '3ZWn7A5KQBUDvbqd1FNtjMj7yf6SIcM0OMLRulJ1Qkd7OH+9JluTu0FLw0P7AupF\n' + + 'BW2O4UUZY4K56W/wK/Je29RSd4/EmnFRBBYj6VvqY2izxCWEiwvKz0BA/+zabUol\n' + + 'eBnHXP3ATKFthBRGoN9kkCkSpoz4t+QTlUazGqJrTX57vjA+Gxdjc9Vhn4Q/Ra2f\n' + + 'c4a01h8fRP+IDVLFzh+AfcQ0Q6Fr/3+D9KUj/poS2O3G4ACfIRm8L2zaVGnfEmiT\n' + + '7T/8ZJBQrHkncXHCbbocB1g0PSFoDrXLafNKaCS2EItk+FBUF3EQKfc/FxUwFXG6\n' + + 'WhPptorUXx+TCcVuoR0ifKEnLEBnhhlwFM09gHRLxPDenSj0WIv/Nm+HP1Nd2410\n' + + 'KvcEVLo/HyJDg7GEIi6Q+EZPasCvI7vxKLBBavrvBAZwRjA2tYVYYadUnlpuMdB3\n' + + '97iY+tPZ31OjBLl7Ho3BlA7W3yd8ptuqAcvhgBpBGBDc4rW02Ohb9DcTpzZioQIl\n' + + 'Ih6p2vgIOZKz2cnJ+0sXf8xiRyPfkJE71eRkpQC9bdnddENEROXzAx80wP7kkajE\n' + + 'W8CD9LLMZC65+X4sg+0g+RDnCqgYk2XoKnBaJC1qdaMQ3OrdGoPQsjk1Bq+qyk9Q\n' + + '7CqjzK897IdV5g+OBRbHi78gvF04Ruqgnq9lPz0OfjAxlDBoGYfAUsbRJKIXbGPq\n' + + 'e57SbUkrsQXYLlbsj0vFb5z/MTveFAarbJ1/TPUYuvf9Z9w7S3qz/H8fc72fDuYM\n' + + 'oI36H4NIou/7Jh+92CA27i+eamIZ8p5Ql28rEHpNB1qfIFoO0x1u7/1P2Mq7CbfF\n' + + 'H0bg2KrSb5VkDnfHAnAF/hkt4K1yD0RcSD1abkC07cEzRmIQ95mtuX08sia3Yn0C\n' + + 'dwc4gOeR+oiHxAsyV3wvrm8/w4AAqSbBqtxafAJ44dXJsyoRSRt1vkPta1IUUFZ6\n' + + 'I+jv5nMv16jaJq6IpsI5ujxl/tKbniWC0Jjw5LqoT3beWaZ91iU=\n' + + '=AkaP\n' + + '-----END PGP MESSAGE-----\n' +}; + +// eslint-disable-next-line no-unused-vars +const filename_files = [{ + name: 'Example-1234.txt', + data: '-----BEGIN PGP MESSAGE-----\n' + + '\n' + + 'hQEMA6B8jfIUScGEAQf/Ylt9GDcv/PGjX8v8CBWIeetzD7DpB8c5dZu57rPOhF7a\n' + + 'gZ5wUCNwuZ5jSnPh/MAH1amr9AEHhW28JlHq+Lpoohl50iNFQy01M+Kxh1LmSKup\n' + + 'hFQl3Lu+NewdShq/RwNc9+qdTAnCdwjGJ+SxODfo73cflLl9SSPJ7k29bdUUL1mp\n' + + 'aGlYdecTB6lcz4pCNOyyGryDBJQcS5ObulpN4zvhSfFzT27GQFmQPElm7CTdGOf0\n' + + '5VUxFe0TqRmdJ9LzVuOVZB7x8E0BpuQYpPd88emS+KOozx4KWu0IakdQ4QBY0av5\n' + + 'ZID2rgM640Z4T8kXgGZq2qFN1Ap5X3iwfjkEHaJIP9JXAb86F8IP7nLrxzN2V0eM\n' + + '3v0+1o0HJd/E4LPeXHXCaNDaJOr8rviOCLwoFvCJ9E10ZASLyqOXzhlW9Tkvxrjl\n' + + 'ldeXQI8Fp6oWPfvW8qGQ917mzxuoQYGn\n' + + '=993W\n' + + '-----END PGP MESSAGE-----\n' +}, { + name: 'Example-@€µ2äüß.txt', + data: '-----BEGIN PGP MESSAGE-----\n'+ + '\n'+ + 'hQEMA6B8jfIUScGEAQgAiX5vBNJGPYvljleo/7nkee4mGsFL1ROXLOs7sUlBImFm\n'+ + 'axQ0PAtVsX9NvDY70Tj5EIaGmgQWr/WAnH5fuV+ctsZtPm/UsL2BhYgKz3cDcS2P\n'+ + '1tni3WhHXVr8ldC3PePuEn0Wfy/wOS+y2FbkJOD9EqXeui06phB8ScGdF6se3AcA\n'+ + 'lNo6bFeURgK6NhIYgibKbybAr1+D/zUvksn5xnLztBarVeJFOwAj8I+lthLpoyj2\n'+ + 'vUFu2qOlSOW/98Z0ZYDvRqnB5Mqmqsgf0cWl4Lwt0+GrdfzuB+479+ouIJCFUaIA\n'+ + 'JDoU8Ct0UwgAoYZmDkxBtjZALmf3dGqH1gjSe0UbDdJhAZ9h5rlC525JNOse0v21\n'+ + 'LdrDtwtiETFZ9ras8RelYeyYyE7PfhBxtmP5EBZUk7Be6JbD2vn5s2pgsbmBTzGJ\n'+ + 'AcxxSN6MbTvInIvC3GhSTs0mLiC4sToVoPp/F8tfQIGZWg==\n'+ + '=V6wP\n'+ + '-----END PGP MESSAGE-----\n' +}, { + name: 'Example- äüüß.txt', + data: '-----BEGIN PGP MESSAGE-----\n' + + '\n' + + 'hQEMA6B8jfIUScGEAQf9H7CbkI952WbUqkuYIlgKri+Tr+G+9m1GN/mKh82GnwfZ\n' + + '8JekOOzdZ6BdCfyJohOSan959r1pOHJzj2sh+LitBbD02MDPg8BL14lUXfbUju7s\n' + + 'eT5HuVDfnFWV2ThfEyVUNmAEaE57FwTzdO7vN1VYkkBNFC8pjCONQ6/iRWnDgUyB\n' + + 'fJJSLkdFMDBgHSrEeSCyDP4P5rJyd/1JhqXXECLIMzIKWCUbvWNvKLfA71fhPbi3\n' + + 'XzXLWhNKQWoMZsl2oEHJuPY7ez/KePJ07Km0gxcbBJhUGTRRNrHSjOxiaV7/TLp2\n' + + 'O3U/GuPQ/eY4Xl3rE/cDaCjy2sdR4VyuxlbLeUVIvtJbAUzNkaibs9ydZshBj9UD\n' + + 'x2JWCwkBa7Q1Mah9nciT8S2Co71dsVMdIc3VtsXUtlhomL1bHd8ipRhFSiqiyZM3\n' + + 'Pih6tFUOcXuSaf0lv6FENXP+IThHiaujtjAbkA==\n' + + '=UxvV\n' + + '-----END PGP MESSAGE-----\n' +}]; diff --git a/lang/js/BrowserTestExtension/tests/longRunningTests.js b/lang/js/BrowserTestExtension/tests/longRunningTests.js new file mode 100644 index 0000000..534a95a --- /dev/null +++ b/lang/js/BrowserTestExtension/tests/longRunningTests.js @@ -0,0 +1,58 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ +/* global describe, it, before, expect, Gpgmejs */ +/* global bigString, inputvalues */ + +describe('Long running Encryption/Decryption', function () { + let context = null; + const good_fpr = inputvalues.encrypt.good.fingerprint; + before(function (done){ + const prm = Gpgmejs.init({ timeout: 2000 }); + prm.then(function (gpgmejs){ + context = gpgmejs; + done(); + }); + }); + + for (let i=1; i < 101; i++) { + it('Successful encrypt/decrypt completely random data ' + + (i) + '/100', function (done) { + const data = bigString(2*1024*1024); + context.encrypt({ data: data, publicKeys: good_fpr }) + .then(function (answer){ + expect(answer).to.not.be.empty; + expect(answer.data).to.be.a('string'); + expect(answer.data).to.include('BEGIN PGP MESSAGE'); + expect(answer.data).to.include('END PGP MESSAGE'); + context.decrypt({ data: answer.data }) + .then(function (result){ + expect(result).to.not.be.empty; + expect(result.data).to.be.a('string'); + expect(result.data).to.equal(data); + done(); + }); + }); + }).timeout(15000); + } + +}); diff --git a/lang/js/BrowserTestExtension/tests/signTest.js b/lang/js/BrowserTestExtension/tests/signTest.js new file mode 100644 index 0000000..1e269e6 --- /dev/null +++ b/lang/js/BrowserTestExtension/tests/signTest.js @@ -0,0 +1,64 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +/* global describe, it, expect, before, Gpgmejs */ +/* global bigString, inputvalues */ + +describe('Signing', function () { + let context = null; + const good_fpr = inputvalues.encrypt.good.fingerprint; + + before(function (done){ + const prm = Gpgmejs.init({ timeout: 2000 }); + prm.then(function (gpgmejs){ + context = gpgmejs; + done(); + }); + }); + + it('Sign a message', function (done) { + const data = bigString(100); + context.sign({ data: data, keys: good_fpr }).then(function (answer) { + expect(answer).to.not.be.empty; + expect(answer.data).to.be.a('string'); + expect(answer.data).to.include('BEGIN PGP SIGNATURE'); + expect(answer.data).to.include('END PGP SIGNATURE'); + expect(answer.data).to.include(data); + done(); + }); + }); + + it('Detached sign a message', function (done) { + const data = bigString(100); + context.sign({ data: data, keys: good_fpr, mode: 'detached' }) + .then(function (answer) { + expect(answer).to.not.be.empty; + expect(answer.data).to.be.a('string'); + expect(answer.data).to.include(data); + expect(answer.signature).to.be.a('string'); + expect(answer.signature).to.be.a('string'); + done(); + }); + }); + +}); diff --git a/lang/js/BrowserTestExtension/tests/startup.js b/lang/js/BrowserTestExtension/tests/startup.js new file mode 100644 index 0000000..e7c7478 --- /dev/null +++ b/lang/js/BrowserTestExtension/tests/startup.js @@ -0,0 +1,47 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +/* global describe, it, expect, Gpgmejs, inputvalues */ + +describe('GPGME context', function (){ + it('Starting a GpgME instance', function (done){ + let prm = Gpgmejs.init({ timeout: 2000 }); + const input = inputvalues.someInputParameter; + prm.then( + function (context){ + expect(context).to.be.an('object'); + expect(context.encrypt).to.be.a('function'); + expect(context.decrypt).to.be.a('function'); + expect(context.sign).to.be.a('function'); + expect(context.verify).to.be.a('function'); + context.Keyring = input; + expect(context.Keyring).to.be.an('object'); + expect(context.Keyring).to.not.equal(input); + expect(context.Keyring.getKeys).to.be.a('function'); + expect(context.Keyring.getDefaultKey).to.be.a('function'); + expect(context.Keyring.importKey).to.be.a('function'); + expect(context.Keyring.generateKey).to.be.a('function'); + done(); + }); + }); +});
\ No newline at end of file diff --git a/lang/js/BrowserTestExtension/tests/verifyTest.js b/lang/js/BrowserTestExtension/tests/verifyTest.js new file mode 100644 index 0000000..c63f684 --- /dev/null +++ b/lang/js/BrowserTestExtension/tests/verifyTest.js @@ -0,0 +1,90 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +/* global describe, it, expect, before, bigString, inputvalues, Gpgmejs */ + + + +describe('Verifying data', function () { + let context = null; + before(function (done){ + const prm = Gpgmejs.init({ timeout: 2000 }); + prm.then(function (gpgmejs){ + context = gpgmejs; + done(); + }); + }); + it('Successful verify message', function (done) { + const message = inputvalues.signedMessage.good; + context.verify({ data: message }).then(function (result){ + expect(result.data).to.be.a('string'); + expect(result.signatures.all_valid).to.be.true; + expect(result.signatures.count).to.equal(1); + expect(result.signatures.signatures.good).to.be.an('array'); + expect(result.signatures.signatures.good.length).to.equal(1); + expect(result.signatures.signatures.good[0].fingerprint).to.be.a('string'); + expect(result.signatures.signatures.good[0].valid).to.be.true; + done(); + }); + }); + + it('Successfully recognize changed cleartext', function (done) { + const message = inputvalues.signedMessage.bad; + context.verify({ data: message }).then(function (result){ + expect(result.data).to.be.a('string'); + expect(result.signatures.all_valid).to.be.false; + expect(result.signatures.count).to.equal(1); + expect(result.signatures.signatures.bad).to.be.an('array'); + expect(result.signatures.signatures.bad.length).to.equal(1); + expect(result.signatures.signatures.bad[0].fingerprint) + .to.be.a('string'); + expect(result.signatures.signatures.bad[0].valid) + .to.be.false; + done(); + }); + }); + + it('Encrypt-Sign-Verify random message', function (done) { + const message = bigString(2000); + let fpr = inputvalues.encrypt.good.fingerprint; + context.encrypt({ data: message, publicKeys: fpr }) + .then(function (message_enc){ + context.sign({ data: message_enc.data, keys: fpr }) + .then(function (message_encsign){ + context.verify({ data: message_encsign.data }) + .then(function (result){ + expect(result.data).to.equal(message_enc.data); + expect(result.data).to.be.a('string'); + expect(result.signatures.all_valid).to.be.true; + expect(result.signatures.count).to.equal(1); + const arr = result.signatures.signatures.good; + expect(arr).to.be.an('array'); + expect(arr.length).to.equal(1); + expect(arr[0].fingerprint).to.equal(fpr); + expect(arr[0].valid).to.be.true; + done(); + }); + }); + }); + }); +});
\ No newline at end of file diff --git a/lang/js/BrowserTestExtension/unittests.html b/lang/js/BrowserTestExtension/unittests.html new file mode 100644 index 0000000..6f7da3f --- /dev/null +++ b/lang/js/BrowserTestExtension/unittests.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <link href="libs/mocha.css" rel="stylesheet" /> + + </head> +<body> + <h3>Unit tests</h3> + <div id="mocha"></div> + <script src="libs/mocha.js"></script> + <script src="libs/chai.js"></script> + <script src="setup_testing.js"></script> + <script src="libs/gpgmejs_unittests.bundle.js"></script> + <script src="rununittests.js"></script> + </body> +</html> diff --git a/lang/js/DemoExtension/Makefile.am b/lang/js/DemoExtension/Makefile.am new file mode 100644 index 0000000..d6e87fd --- /dev/null +++ b/lang/js/DemoExtension/Makefile.am @@ -0,0 +1,27 @@ +# Makefile.am for gpgme.js. +# Copyright (C) 2018 Intevation GmbH +# +# This file is part of gpgme.js. +# +# gpgme.js is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# gpgme.js 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA +# 02111-1307, USA + +EXTRA_DIST = manifest.json \ + popup.html \ + entry.js \ + maindemo.js \ + mainui.html \ + testicon.png \ + ui.css diff --git a/lang/js/DemoExtension/Makefile.in b/lang/js/DemoExtension/Makefile.in new file mode 100644 index 0000000..1a379fd --- /dev/null +++ b/lang/js/DemoExtension/Makefile.in @@ -0,0 +1,537 @@ +# Makefile.in generated by automake 1.14.1 from Makefile.am. +# @configure_input@ + +# Copyright (C) 1994-2013 Free Software Foundation, Inc. + +# This Makefile.in is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. + +@SET_MAKE@ + +# Makefile.am for gpgme.js. +# Copyright (C) 2018 Intevation GmbH +# +# This file is part of gpgme.js. +# +# gpgme.js is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# gpgme.js 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA +# 02111-1307, USA +VPATH = @srcdir@ +am__is_gnu_make = test -n '$(MAKEFILE_LIST)' && test -n '$(MAKELEVEL)' +am__make_running_with_option = \ + case $${target_option-} in \ + ?) ;; \ + *) echo "am__make_running_with_option: internal error: invalid" \ + "target option '$${target_option-}' specified" >&2; \ + exit 1;; \ + esac; \ + has_opt=no; \ + sane_makeflags=$$MAKEFLAGS; \ + if $(am__is_gnu_make); then \ + sane_makeflags=$$MFLAGS; \ + else \ + case $$MAKEFLAGS in \ + *\\[\ \ ]*) \ + bs=\\; \ + sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \ + | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \ + esac; \ + fi; \ + skip_next=no; \ + strip_trailopt () \ + { \ + flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \ + }; \ + for flg in $$sane_makeflags; do \ + test $$skip_next = yes && { skip_next=no; continue; }; \ + case $$flg in \ + *=*|--*) continue;; \ + -*I) strip_trailopt 'I'; skip_next=yes;; \ + -*I?*) strip_trailopt 'I';; \ + -*O) strip_trailopt 'O'; skip_next=yes;; \ + -*O?*) strip_trailopt 'O';; \ + -*l) strip_trailopt 'l'; skip_next=yes;; \ + -*l?*) strip_trailopt 'l';; \ + -[dEDm]) skip_next=yes;; \ + -[JT]) skip_next=yes;; \ + esac; \ + case $$flg in \ + *$$target_option*) has_opt=yes; break;; \ + esac; \ + done; \ + test $$has_opt = yes +am__make_dryrun = (target_option=n; $(am__make_running_with_option)) +am__make_keepgoing = (target_option=k; $(am__make_running_with_option)) +pkgdatadir = $(datadir)/@PACKAGE@ +pkgincludedir = $(includedir)/@PACKAGE@ +pkglibdir = $(libdir)/@PACKAGE@ +pkglibexecdir = $(libexecdir)/@PACKAGE@ +am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd +install_sh_DATA = $(install_sh) -c -m 644 +install_sh_PROGRAM = $(install_sh) -c +install_sh_SCRIPT = $(install_sh) -c +INSTALL_HEADER = $(INSTALL_DATA) +transform = $(program_transform_name) +NORMAL_INSTALL = : +PRE_INSTALL = : +POST_INSTALL = : +NORMAL_UNINSTALL = : +PRE_UNINSTALL = : +POST_UNINSTALL = : +build_triplet = @build@ +host_triplet = @host@ +subdir = lang/js/DemoExtension +DIST_COMMON = $(srcdir)/Makefile.in $(srcdir)/Makefile.am \ + $(top_srcdir)/build-aux/mkinstalldirs +ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 +am__aclocal_m4_deps = $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ + $(top_srcdir)/m4/ax_pkg_swig.m4 \ + $(top_srcdir)/m4/ax_python_devel.m4 \ + $(top_srcdir)/m4/glib-2.0.m4 $(top_srcdir)/m4/glibc21.m4 \ + $(top_srcdir)/m4/gnupg-ttyname.m4 \ + $(top_srcdir)/m4/gpg-error.m4 $(top_srcdir)/m4/libassuan.m4 \ + $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/ltoptions.m4 \ + $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \ + $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/pkg.m4 \ + $(top_srcdir)/m4/python.m4 $(top_srcdir)/m4/qt.m4 \ + $(top_srcdir)/acinclude.m4 $(top_srcdir)/configure.ac +am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ + $(ACLOCAL_M4) +mkinstalldirs = $(SHELL) $(top_srcdir)/build-aux/mkinstalldirs +CONFIG_HEADER = $(top_builddir)/conf/config.h +CONFIG_CLEAN_FILES = +CONFIG_CLEAN_VPATH_FILES = +AM_V_P = $(am__v_P_@AM_V@) +am__v_P_ = $(am__v_P_@AM_DEFAULT_V@) +am__v_P_0 = false +am__v_P_1 = : +AM_V_GEN = $(am__v_GEN_@AM_V@) +am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@) +am__v_GEN_0 = @echo " GEN " $@; +am__v_GEN_1 = +AM_V_at = $(am__v_at_@AM_V@) +am__v_at_ = $(am__v_at_@AM_DEFAULT_V@) +am__v_at_0 = @ +am__v_at_1 = +SOURCES = +DIST_SOURCES = +am__can_run_installinfo = \ + case $$AM_UPDATE_INFO_DIR in \ + n|no|NO) false;; \ + *) (install-info --version) >/dev/null 2>&1;; \ + esac +am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP) +DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +ACLOCAL = @ACLOCAL@ +AMTAR = @AMTAR@ +AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@ +AR = @AR@ +AS = @AS@ +AUTOCONF = @AUTOCONF@ +AUTOHEADER = @AUTOHEADER@ +AUTOMAKE = @AUTOMAKE@ +AWK = @AWK@ +BUILD_FILEVERSION = @BUILD_FILEVERSION@ +BUILD_REVISION = @BUILD_REVISION@ +BUILD_TIMESTAMP = @BUILD_TIMESTAMP@ +CC = @CC@ +CCDEPMODE = @CCDEPMODE@ +CC_FOR_BUILD = @CC_FOR_BUILD@ +CFLAGS = @CFLAGS@ +CPP = @CPP@ +CPPFLAGS = @CPPFLAGS@ +CXX = @CXX@ +CXXCPP = @CXXCPP@ +CXXDEPMODE = @CXXDEPMODE@ +CXXFLAGS = @CXXFLAGS@ +CYGPATH_W = @CYGPATH_W@ +DEFS = @DEFS@ +DEPDIR = @DEPDIR@ +DLLTOOL = @DLLTOOL@ +DOXYGEN = @DOXYGEN@ +DSYMUTIL = @DSYMUTIL@ +DUMPBIN = @DUMPBIN@ +ECHO_C = @ECHO_C@ +ECHO_N = @ECHO_N@ +ECHO_T = @ECHO_T@ +EGREP = @EGREP@ +ENABLED_LANGUAGES = @ENABLED_LANGUAGES@ +EXEEXT = @EXEEXT@ +FGREP = @FGREP@ +GITLOG_TO_CHANGELOG = @GITLOG_TO_CHANGELOG@ +GLIBC21 = @GLIBC21@ +GLIB_CFLAGS = @GLIB_CFLAGS@ +GLIB_GENMARSHAL = @GLIB_GENMARSHAL@ +GLIB_LIBS = @GLIB_LIBS@ +GLIB_MKENUMS = @GLIB_MKENUMS@ +GOBJECT_QUERY = @GOBJECT_QUERY@ +GPGME_CONFIG_API_VERSION = @GPGME_CONFIG_API_VERSION@ +GPGME_CONFIG_AVAIL_LANG = @GPGME_CONFIG_AVAIL_LANG@ +GPGME_CONFIG_CFLAGS = @GPGME_CONFIG_CFLAGS@ +GPGME_CONFIG_HOST = @GPGME_CONFIG_HOST@ +GPGME_CONFIG_LIBS = @GPGME_CONFIG_LIBS@ +GPGME_QTTEST_CFLAGS = @GPGME_QTTEST_CFLAGS@ +GPGME_QTTEST_LIBS = @GPGME_QTTEST_LIBS@ +GPGME_QT_CFLAGS = @GPGME_QT_CFLAGS@ +GPGME_QT_LIBS = @GPGME_QT_LIBS@ +GPG_ERROR_CFLAGS = @GPG_ERROR_CFLAGS@ +GPG_ERROR_CONFIG = @GPG_ERROR_CONFIG@ +GPG_ERROR_LIBS = @GPG_ERROR_LIBS@ +GPG_ERROR_MT_CFLAGS = @GPG_ERROR_MT_CFLAGS@ +GPG_ERROR_MT_LIBS = @GPG_ERROR_MT_LIBS@ +GRAPHVIZ = @GRAPHVIZ@ +GREP = @GREP@ +HAVE_CXX11 = @HAVE_CXX11@ +HAVE_DOT = @HAVE_DOT@ +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ +INSTALL_PROGRAM = @INSTALL_PROGRAM@ +INSTALL_SCRIPT = @INSTALL_SCRIPT@ +INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@ +LD = @LD@ +LDFLAGS = @LDFLAGS@ +LIBASSUAN_CFLAGS = @LIBASSUAN_CFLAGS@ +LIBASSUAN_CONFIG = @LIBASSUAN_CONFIG@ +LIBASSUAN_LIBS = @LIBASSUAN_LIBS@ +LIBGPGMEPP_LT_AGE = @LIBGPGMEPP_LT_AGE@ +LIBGPGMEPP_LT_CURRENT = @LIBGPGMEPP_LT_CURRENT@ +LIBGPGMEPP_LT_REVISION = @LIBGPGMEPP_LT_REVISION@ +LIBGPGME_LT_AGE = @LIBGPGME_LT_AGE@ +LIBGPGME_LT_CURRENT = @LIBGPGME_LT_CURRENT@ +LIBGPGME_LT_REVISION = @LIBGPGME_LT_REVISION@ +LIBOBJS = @LIBOBJS@ +LIBQGPGME_LT_AGE = @LIBQGPGME_LT_AGE@ +LIBQGPGME_LT_CURRENT = @LIBQGPGME_LT_CURRENT@ +LIBQGPGME_LT_REVISION = @LIBQGPGME_LT_REVISION@ +LIBS = @LIBS@ +LIBTOOL = @LIBTOOL@ +LIPO = @LIPO@ +LN_S = @LN_S@ +LTLIBOBJS = @LTLIBOBJS@ +MAINT = @MAINT@ +MAKEINFO = @MAKEINFO@ +MANIFEST_TOOL = @MANIFEST_TOOL@ +MKDIR_P = @MKDIR_P@ +MOC = @MOC@ +MOC2 = @MOC2@ +NEED__FILE_OFFSET_BITS = @NEED__FILE_OFFSET_BITS@ +NM = @NM@ +NMEDIT = @NMEDIT@ +OBJDUMP = @OBJDUMP@ +OBJEXT = @OBJEXT@ +OTOOL = @OTOOL@ +OTOOL64 = @OTOOL64@ +PACKAGE = @PACKAGE@ +PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@ +PACKAGE_NAME = @PACKAGE_NAME@ +PACKAGE_STRING = @PACKAGE_STRING@ +PACKAGE_TARNAME = @PACKAGE_TARNAME@ +PACKAGE_URL = @PACKAGE_URL@ +PACKAGE_VERSION = @PACKAGE_VERSION@ +PATH_SEPARATOR = @PATH_SEPARATOR@ +PKG_CONFIG = @PKG_CONFIG@ +PYTHON = @PYTHON@ +PYTHONS = @PYTHONS@ +PYTHON_CPPFLAGS = @PYTHON_CPPFLAGS@ +PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@ +PYTHON_EXTRA_LDFLAGS = @PYTHON_EXTRA_LDFLAGS@ +PYTHON_EXTRA_LIBS = @PYTHON_EXTRA_LIBS@ +PYTHON_LDFLAGS = @PYTHON_LDFLAGS@ +PYTHON_PLATFORM = @PYTHON_PLATFORM@ +PYTHON_PREFIX = @PYTHON_PREFIX@ +PYTHON_SITE_PKG = @PYTHON_SITE_PKG@ +PYTHON_VERSION = @PYTHON_VERSION@ +QTCHOOSER = @QTCHOOSER@ +RANLIB = @RANLIB@ +RC = @RC@ +SED = @SED@ +SET_MAKE = @SET_MAKE@ +SHELL = @SHELL@ +STRIP = @STRIP@ +SWIG = @SWIG@ +SWIG_LIB = @SWIG_LIB@ +SYSROOT = @SYSROOT@ +VERSION = @VERSION@ +VERSION_MAJOR = @VERSION_MAJOR@ +VERSION_MICRO = @VERSION_MICRO@ +VERSION_MINOR = @VERSION_MINOR@ +VERSION_NUMBER = @VERSION_NUMBER@ +abs_builddir = @abs_builddir@ +abs_srcdir = @abs_srcdir@ +abs_top_builddir = @abs_top_builddir@ +abs_top_srcdir = @abs_top_srcdir@ +ac_ct_AR = @ac_ct_AR@ +ac_ct_CC = @ac_ct_CC@ +ac_ct_CXX = @ac_ct_CXX@ +ac_ct_DUMPBIN = @ac_ct_DUMPBIN@ +am__include = @am__include@ +am__leading_dot = @am__leading_dot@ +am__quote = @am__quote@ +am__tar = @am__tar@ +am__untar = @am__untar@ +bindir = @bindir@ +build = @build@ +build_alias = @build_alias@ +build_cpu = @build_cpu@ +build_os = @build_os@ +build_vendor = @build_vendor@ +builddir = @builddir@ +datadir = @datadir@ +datarootdir = @datarootdir@ +docdir = @docdir@ +dvidir = @dvidir@ +emacs_local_vars_begin = @emacs_local_vars_begin@ +emacs_local_vars_end = @emacs_local_vars_end@ +emacs_local_vars_read_only = @emacs_local_vars_read_only@ +exec_prefix = @exec_prefix@ +host = @host@ +host_alias = @host_alias@ +host_cpu = @host_cpu@ +host_os = @host_os@ +host_vendor = @host_vendor@ +htmldir = @htmldir@ +includedir = @includedir@ +infodir = @infodir@ +install_sh = @install_sh@ +libdir = @libdir@ +libexecdir = @libexecdir@ +localedir = @localedir@ +localstatedir = @localstatedir@ +mandir = @mandir@ +mkdir_p = @mkdir_p@ +oldincludedir = @oldincludedir@ +pdfdir = @pdfdir@ +pkgpyexecdir = @pkgpyexecdir@ +pkgpythondir = @pkgpythondir@ +prefix = @prefix@ +program_transform_name = @program_transform_name@ +psdir = @psdir@ +pyexecdir = @pyexecdir@ +pythondir = @pythondir@ +sbindir = @sbindir@ +sharedstatedir = @sharedstatedir@ +srcdir = @srcdir@ +sysconfdir = @sysconfdir@ +target_alias = @target_alias@ +top_build_prefix = @top_build_prefix@ +top_builddir = @top_builddir@ +top_srcdir = @top_srcdir@ +EXTRA_DIST = manifest.json \ + popup.html \ + entry.js \ + maindemo.js \ + mainui.html \ + testicon.png \ + ui.css + +all: all-am + +.SUFFIXES: +$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps) + @for dep in $?; do \ + case '$(am__configure_deps)' in \ + *$$dep*) \ + ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \ + && { if test -f $@; then exit 0; else break; fi; }; \ + exit 1;; \ + esac; \ + done; \ + echo ' cd $(top_srcdir) && $(AUTOMAKE) --gnu lang/js/DemoExtension/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --gnu lang/js/DemoExtension/Makefile +.PRECIOUS: Makefile +Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status + @case '$?' in \ + *config.status*) \ + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ + *) \ + echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe)'; \ + cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe);; \ + esac; + +$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh + +$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(am__aclocal_m4_deps): + +mostlyclean-libtool: + -rm -f *.lo + +clean-libtool: + -rm -rf .libs _libs +tags TAGS: + +ctags CTAGS: + +cscope cscopelist: + + +distdir: $(DISTFILES) + @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + list='$(DISTFILES)'; \ + dist_files=`for file in $$list; do echo $$file; done | \ + sed -e "s|^$$srcdirstrip/||;t" \ + -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ + case $$dist_files in \ + */*) $(MKDIR_P) `echo "$$dist_files" | \ + sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ + sort -u` ;; \ + esac; \ + for file in $$dist_files; do \ + if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ + if test -d $$d/$$file; then \ + dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ + if test -d "$(distdir)/$$file"; then \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ + cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ + else \ + test -f "$(distdir)/$$file" \ + || cp -p $$d/$$file "$(distdir)/$$file" \ + || exit 1; \ + fi; \ + done +check-am: all-am +check: check-am +all-am: Makefile +installdirs: +install: install-am +install-exec: install-exec-am +install-data: install-data-am +uninstall: uninstall-am + +install-am: all-am + @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am + +installcheck: installcheck-am +install-strip: + if test -z '$(STRIP)'; then \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + install; \ + else \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \ + fi +mostlyclean-generic: + +clean-generic: + +distclean-generic: + -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES) + -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES) + +maintainer-clean-generic: + @echo "This command is intended for maintainers to use" + @echo "it deletes files that may require special tools to rebuild." +clean: clean-am + +clean-am: clean-generic clean-libtool mostlyclean-am + +distclean: distclean-am + -rm -f Makefile +distclean-am: clean-am distclean-generic + +dvi: dvi-am + +dvi-am: + +html: html-am + +html-am: + +info: info-am + +info-am: + +install-data-am: + +install-dvi: install-dvi-am + +install-dvi-am: + +install-exec-am: + +install-html: install-html-am + +install-html-am: + +install-info: install-info-am + +install-info-am: + +install-man: + +install-pdf: install-pdf-am + +install-pdf-am: + +install-ps: install-ps-am + +install-ps-am: + +installcheck-am: + +maintainer-clean: maintainer-clean-am + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic + +mostlyclean: mostlyclean-am + +mostlyclean-am: mostlyclean-generic mostlyclean-libtool + +pdf: pdf-am + +pdf-am: + +ps: ps-am + +ps-am: + +uninstall-am: + +.MAKE: install-am install-strip + +.PHONY: all all-am check check-am clean clean-generic clean-libtool \ + cscopelist-am ctags-am distclean distclean-generic \ + distclean-libtool distdir dvi dvi-am html html-am info info-am \ + install install-am install-data install-data-am install-dvi \ + install-dvi-am install-exec install-exec-am install-html \ + install-html-am install-info install-info-am install-man \ + install-pdf install-pdf-am install-ps install-ps-am \ + install-strip installcheck installcheck-am installdirs \ + maintainer-clean maintainer-clean-generic mostlyclean \ + mostlyclean-generic mostlyclean-libtool pdf pdf-am ps ps-am \ + tags-am uninstall uninstall-am + + +# Tell versions [3.59,3.63) of GNU make to not export all variables. +# Otherwise a system limit (for SysV at least) may be exceeded. +.NOEXPORT: diff --git a/lang/js/DemoExtension/entry.js b/lang/js/DemoExtension/entry.js new file mode 100644 index 0000000..fd261a0 --- /dev/null +++ b/lang/js/DemoExtension/entry.js @@ -0,0 +1,30 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +/* global chrome */ + +document.addEventListener('DOMContentLoaded', function () { + chrome.tabs.create({ + url: './mainui.html' + }); +}); diff --git a/lang/js/DemoExtension/maindemo.js b/lang/js/DemoExtension/maindemo.js new file mode 100644 index 0000000..c992e7e --- /dev/null +++ b/lang/js/DemoExtension/maindemo.js @@ -0,0 +1,123 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +/* global document, Gpgmejs */ + +document.addEventListener('DOMContentLoaded', function () { + Gpgmejs.init().then(function (gpgmejs){ + document.getElementById('buttonencrypt').addEventListener('click', + function (){ + let data = document.getElementById('inputtext').value; + let keyId = document.getElementById('pubkey').value; + gpgmejs.encrypt({ data: data, publicKeys: keyId, armor: true }) + .then(function (answer){ + if (answer.data){ + document.getElementById( + 'answer').value = answer.data; + } + }, function (errormsg){ + alert( errormsg.message); + }); + }); + + document.getElementById('buttondecrypt').addEventListener('click', + function (){ + let data = document.getElementById('inputtext').value; + gpgmejs.decrypt({ data: data }).then( + function (answer){ + if (answer.data){ + document.getElementById( + 'answer').value = answer.data; + } + }, function (errormsg){ + alert(errormsg.message); + }); + }); + + document.getElementById('getdefaultkey').addEventListener('click', + function (){ + gpgmejs.Keyring.getDefaultKey().then(function (answer){ + document.getElementById('pubkey').value = + answer.fingerprint; + }, function (errormsg){ + alert(errormsg.message); + }); + }); + + document.getElementById('signtext').addEventListener('click', + function (){ + let data = document.getElementById('inputtext').value; + let keyId = document.getElementById('pubkey').value; + gpgmejs.sign({ data: data, keys: keyId }).then( + function (answer){ + if (answer.data){ + document.getElementById( + 'answer').value = answer.data; + } + }, function (errormsg){ + alert( errormsg.message); + }); + }); + + document.getElementById('verifytext').addEventListener('click', + function (){ + let data = document.getElementById('inputtext').value; + gpgmejs.verify({ data: data }).then( + function (answer){ + let vals = ''; + if (answer.all_valid === true){ + vals = 'Success! '; + } else { + vals = 'Failure! '; + } + vals = vals + (answer.count - answer.failures) + 'of ' + + answer.count + ' signature(s) were successfully ' + + 'verified.\n\n' + answer.data; + document.getElementById('answer').value = vals; + }, function (errormsg){ + alert( errormsg.message); + }); + }); + document.getElementById('searchkey').addEventListener('click', + function (){ + let data = document.getElementById('inputtext').value; + gpgmejs.Keyring.getKeys({ + pattern: data, + prepare_sync: true, + search: true } + ).then(function (keys){ + if (keys.length === 1){ + document.getElementById( + 'pubkey').value = keys[0].fingerprint; + } else if (keys.length > 1) { + alert('The pattern was not unambigious enough for a Key. ' + + keys.length + ' Keys were found'); + } else { + alert('No keys found'); + } + }, function (errormsg){ + alert( errormsg.message); + }); + }); + }); +}); diff --git a/lang/js/DemoExtension/mainui.html b/lang/js/DemoExtension/mainui.html new file mode 100644 index 0000000..c773c9b --- /dev/null +++ b/lang/js/DemoExtension/mainui.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <link rel="stylesheet" href="ui.css"/> + <script src="libs/gpgmejs.bundle.js"></script> + <script src="maindemo.js"></script> + </head> + <body> + <div> + + <div class="left"> + <ul> + <li> + <span class="label">Input</span> + <textarea rows="5" cols="65" id="inputtext" wrap="hard"></textarea> + </li> + <li> + <span class="label">Fingerprint of Key to use: </span> + <input type="text" id="pubkey" value="" /> + <button id="getdefaultkey"> + Set to default signing key + </button> + <button id="searchkey"> + Look up Key + </button> + </li> + </ul> + </div> + <div class="right"> + <ul> + <li> + <span class="label">Result</span> + <textarea id="answer" rows="5" cols="65" wrap="hard"></textarea> + </li> + </ul> + </div> + </div> + <div class="center"> + <button id="buttonencrypt">Encrypt input text</button><br> + <button id="buttondecrypt">Decrypt input text</button><br> + <button id="signtext">Sign input text</button> <br> + <button id="verifytext">Verify input text</button><br> + + </div> +</body> +</html> diff --git a/lang/js/DemoExtension/manifest.json b/lang/js/DemoExtension/manifest.json new file mode 100644 index 0000000..9e057b3 --- /dev/null +++ b/lang/js/DemoExtension/manifest.json @@ -0,0 +1,14 @@ +{ + "manifest_version": 2, + + "name": "gpgme-json with native Messaging", + "description": "A simple demo application", + "version": "0.1", + "content_security_policy": "default-src 'self' filesystem:", + "browser_action": { + "default_icon": "testicon.png", + "default_title": "gpgme.js", + "default_popup": "popup.html" + }, + "permissions": ["nativeMessaging", "activeTab"] +} diff --git a/lang/js/DemoExtension/popup.html b/lang/js/DemoExtension/popup.html new file mode 100644 index 0000000..5007031 --- /dev/null +++ b/lang/js/DemoExtension/popup.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <script src="entry.js"></script> + </head> + <body> + </body> +</html>
\ No newline at end of file diff --git a/lang/js/DemoExtension/testicon.png b/lang/js/DemoExtension/testicon.png Binary files differnew file mode 100644 index 0000000..84284e0 --- /dev/null +++ b/lang/js/DemoExtension/testicon.png diff --git a/lang/js/DemoExtension/ui.css b/lang/js/DemoExtension/ui.css new file mode 100644 index 0000000..16dfb5a --- /dev/null +++ b/lang/js/DemoExtension/ui.css @@ -0,0 +1,33 @@ +ul { + list-style-type: none; + padding-left: 0px; +} + +ul li span { + float: left; + width: 120px; + margin-top: 6px; +} + +div .left { + float: left; + align-items: stretch; + width: 40%; +} +div .center { + width: 50%; + align-content: space-between; +} + +div .center button { + align-self: stretch; +} +div .right { + float: right; + align-items: stretch; + width: 40%; +} + +div .bottom { + clear:both; +}
\ No newline at end of file diff --git a/lang/js/Makefile.am b/lang/js/Makefile.am new file mode 100644 index 0000000..63cc41b --- /dev/null +++ b/lang/js/Makefile.am @@ -0,0 +1,32 @@ +# Makefile.am for gpgme.js. +# Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik +# +# This file is part of gpgme.js. +# +# gpgme.js is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# gpgme.js 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA +# 02111-1307, USA + +SUBDIRS = src BrowserTestExtension DemoExtension + +EXTRA_DIST = build_extensions.sh \ + jsdoc.conf \ + jsdoc_index.md \ + .eslintrc.json \ + package.json \ + README \ + unittest_inputvalues.js \ + unittests.js \ + webpack.conf.js \ + webpack.conf_unittests.js diff --git a/lang/js/Makefile.in b/lang/js/Makefile.in new file mode 100644 index 0000000..0dbc0bc --- /dev/null +++ b/lang/js/Makefile.in @@ -0,0 +1,721 @@ +# Makefile.in generated by automake 1.14.1 from Makefile.am. +# @configure_input@ + +# Copyright (C) 1994-2013 Free Software Foundation, Inc. + +# This Makefile.in is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. + +@SET_MAKE@ + +# Makefile.am for gpgme.js. +# Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik +# +# This file is part of gpgme.js. +# +# gpgme.js is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# gpgme.js 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA +# 02111-1307, USA +VPATH = @srcdir@ +am__is_gnu_make = test -n '$(MAKEFILE_LIST)' && test -n '$(MAKELEVEL)' +am__make_running_with_option = \ + case $${target_option-} in \ + ?) ;; \ + *) echo "am__make_running_with_option: internal error: invalid" \ + "target option '$${target_option-}' specified" >&2; \ + exit 1;; \ + esac; \ + has_opt=no; \ + sane_makeflags=$$MAKEFLAGS; \ + if $(am__is_gnu_make); then \ + sane_makeflags=$$MFLAGS; \ + else \ + case $$MAKEFLAGS in \ + *\\[\ \ ]*) \ + bs=\\; \ + sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \ + | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \ + esac; \ + fi; \ + skip_next=no; \ + strip_trailopt () \ + { \ + flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \ + }; \ + for flg in $$sane_makeflags; do \ + test $$skip_next = yes && { skip_next=no; continue; }; \ + case $$flg in \ + *=*|--*) continue;; \ + -*I) strip_trailopt 'I'; skip_next=yes;; \ + -*I?*) strip_trailopt 'I';; \ + -*O) strip_trailopt 'O'; skip_next=yes;; \ + -*O?*) strip_trailopt 'O';; \ + -*l) strip_trailopt 'l'; skip_next=yes;; \ + -*l?*) strip_trailopt 'l';; \ + -[dEDm]) skip_next=yes;; \ + -[JT]) skip_next=yes;; \ + esac; \ + case $$flg in \ + *$$target_option*) has_opt=yes; break;; \ + esac; \ + done; \ + test $$has_opt = yes +am__make_dryrun = (target_option=n; $(am__make_running_with_option)) +am__make_keepgoing = (target_option=k; $(am__make_running_with_option)) +pkgdatadir = $(datadir)/@PACKAGE@ +pkgincludedir = $(includedir)/@PACKAGE@ +pkglibdir = $(libdir)/@PACKAGE@ +pkglibexecdir = $(libexecdir)/@PACKAGE@ +am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd +install_sh_DATA = $(install_sh) -c -m 644 +install_sh_PROGRAM = $(install_sh) -c +install_sh_SCRIPT = $(install_sh) -c +INSTALL_HEADER = $(INSTALL_DATA) +transform = $(program_transform_name) +NORMAL_INSTALL = : +PRE_INSTALL = : +POST_INSTALL = : +NORMAL_UNINSTALL = : +PRE_UNINSTALL = : +POST_UNINSTALL = : +build_triplet = @build@ +host_triplet = @host@ +subdir = lang/js +DIST_COMMON = $(srcdir)/Makefile.in $(srcdir)/Makefile.am \ + $(top_srcdir)/build-aux/mkinstalldirs README +ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 +am__aclocal_m4_deps = $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ + $(top_srcdir)/m4/ax_pkg_swig.m4 \ + $(top_srcdir)/m4/ax_python_devel.m4 \ + $(top_srcdir)/m4/glib-2.0.m4 $(top_srcdir)/m4/glibc21.m4 \ + $(top_srcdir)/m4/gnupg-ttyname.m4 \ + $(top_srcdir)/m4/gpg-error.m4 $(top_srcdir)/m4/libassuan.m4 \ + $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/ltoptions.m4 \ + $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \ + $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/pkg.m4 \ + $(top_srcdir)/m4/python.m4 $(top_srcdir)/m4/qt.m4 \ + $(top_srcdir)/acinclude.m4 $(top_srcdir)/configure.ac +am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ + $(ACLOCAL_M4) +mkinstalldirs = $(SHELL) $(top_srcdir)/build-aux/mkinstalldirs +CONFIG_HEADER = $(top_builddir)/conf/config.h +CONFIG_CLEAN_FILES = +CONFIG_CLEAN_VPATH_FILES = +AM_V_P = $(am__v_P_@AM_V@) +am__v_P_ = $(am__v_P_@AM_DEFAULT_V@) +am__v_P_0 = false +am__v_P_1 = : +AM_V_GEN = $(am__v_GEN_@AM_V@) +am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@) +am__v_GEN_0 = @echo " GEN " $@; +am__v_GEN_1 = +AM_V_at = $(am__v_at_@AM_V@) +am__v_at_ = $(am__v_at_@AM_DEFAULT_V@) +am__v_at_0 = @ +am__v_at_1 = +SOURCES = +DIST_SOURCES = +RECURSIVE_TARGETS = all-recursive check-recursive cscopelist-recursive \ + ctags-recursive dvi-recursive html-recursive info-recursive \ + install-data-recursive install-dvi-recursive \ + install-exec-recursive install-html-recursive \ + install-info-recursive install-pdf-recursive \ + install-ps-recursive install-recursive installcheck-recursive \ + installdirs-recursive pdf-recursive ps-recursive \ + tags-recursive uninstall-recursive +am__can_run_installinfo = \ + case $$AM_UPDATE_INFO_DIR in \ + n|no|NO) false;; \ + *) (install-info --version) >/dev/null 2>&1;; \ + esac +RECURSIVE_CLEAN_TARGETS = mostlyclean-recursive clean-recursive \ + distclean-recursive maintainer-clean-recursive +am__recursive_targets = \ + $(RECURSIVE_TARGETS) \ + $(RECURSIVE_CLEAN_TARGETS) \ + $(am__extra_recursive_targets) +AM_RECURSIVE_TARGETS = $(am__recursive_targets:-recursive=) TAGS CTAGS \ + distdir +am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP) +# Read a list of newline-separated strings from the standard input, +# and print each of them once, without duplicates. Input order is +# *not* preserved. +am__uniquify_input = $(AWK) '\ + BEGIN { nonempty = 0; } \ + { items[$$0] = 1; nonempty = 1; } \ + END { if (nonempty) { for (i in items) print i; }; } \ +' +# Make sure the list of sources is unique. This is necessary because, +# e.g., the same source file might be shared among _SOURCES variables +# for different programs/libraries. +am__define_uniq_tagged_files = \ + list='$(am__tagged_files)'; \ + unique=`for i in $$list; do \ + if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ + done | $(am__uniquify_input)` +ETAGS = etags +CTAGS = ctags +DIST_SUBDIRS = $(SUBDIRS) +DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +am__relativize = \ + dir0=`pwd`; \ + sed_first='s,^\([^/]*\)/.*$$,\1,'; \ + sed_rest='s,^[^/]*/*,,'; \ + sed_last='s,^.*/\([^/]*\)$$,\1,'; \ + sed_butlast='s,/*[^/]*$$,,'; \ + while test -n "$$dir1"; do \ + first=`echo "$$dir1" | sed -e "$$sed_first"`; \ + if test "$$first" != "."; then \ + if test "$$first" = ".."; then \ + dir2=`echo "$$dir0" | sed -e "$$sed_last"`/"$$dir2"; \ + dir0=`echo "$$dir0" | sed -e "$$sed_butlast"`; \ + else \ + first2=`echo "$$dir2" | sed -e "$$sed_first"`; \ + if test "$$first2" = "$$first"; then \ + dir2=`echo "$$dir2" | sed -e "$$sed_rest"`; \ + else \ + dir2="../$$dir2"; \ + fi; \ + dir0="$$dir0"/"$$first"; \ + fi; \ + fi; \ + dir1=`echo "$$dir1" | sed -e "$$sed_rest"`; \ + done; \ + reldir="$$dir2" +ACLOCAL = @ACLOCAL@ +AMTAR = @AMTAR@ +AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@ +AR = @AR@ +AS = @AS@ +AUTOCONF = @AUTOCONF@ +AUTOHEADER = @AUTOHEADER@ +AUTOMAKE = @AUTOMAKE@ +AWK = @AWK@ +BUILD_FILEVERSION = @BUILD_FILEVERSION@ +BUILD_REVISION = @BUILD_REVISION@ +BUILD_TIMESTAMP = @BUILD_TIMESTAMP@ +CC = @CC@ +CCDEPMODE = @CCDEPMODE@ +CC_FOR_BUILD = @CC_FOR_BUILD@ +CFLAGS = @CFLAGS@ +CPP = @CPP@ +CPPFLAGS = @CPPFLAGS@ +CXX = @CXX@ +CXXCPP = @CXXCPP@ +CXXDEPMODE = @CXXDEPMODE@ +CXXFLAGS = @CXXFLAGS@ +CYGPATH_W = @CYGPATH_W@ +DEFS = @DEFS@ +DEPDIR = @DEPDIR@ +DLLTOOL = @DLLTOOL@ +DOXYGEN = @DOXYGEN@ +DSYMUTIL = @DSYMUTIL@ +DUMPBIN = @DUMPBIN@ +ECHO_C = @ECHO_C@ +ECHO_N = @ECHO_N@ +ECHO_T = @ECHO_T@ +EGREP = @EGREP@ +ENABLED_LANGUAGES = @ENABLED_LANGUAGES@ +EXEEXT = @EXEEXT@ +FGREP = @FGREP@ +GITLOG_TO_CHANGELOG = @GITLOG_TO_CHANGELOG@ +GLIBC21 = @GLIBC21@ +GLIB_CFLAGS = @GLIB_CFLAGS@ +GLIB_GENMARSHAL = @GLIB_GENMARSHAL@ +GLIB_LIBS = @GLIB_LIBS@ +GLIB_MKENUMS = @GLIB_MKENUMS@ +GOBJECT_QUERY = @GOBJECT_QUERY@ +GPGME_CONFIG_API_VERSION = @GPGME_CONFIG_API_VERSION@ +GPGME_CONFIG_AVAIL_LANG = @GPGME_CONFIG_AVAIL_LANG@ +GPGME_CONFIG_CFLAGS = @GPGME_CONFIG_CFLAGS@ +GPGME_CONFIG_HOST = @GPGME_CONFIG_HOST@ +GPGME_CONFIG_LIBS = @GPGME_CONFIG_LIBS@ +GPGME_QTTEST_CFLAGS = @GPGME_QTTEST_CFLAGS@ +GPGME_QTTEST_LIBS = @GPGME_QTTEST_LIBS@ +GPGME_QT_CFLAGS = @GPGME_QT_CFLAGS@ +GPGME_QT_LIBS = @GPGME_QT_LIBS@ +GPG_ERROR_CFLAGS = @GPG_ERROR_CFLAGS@ +GPG_ERROR_CONFIG = @GPG_ERROR_CONFIG@ +GPG_ERROR_LIBS = @GPG_ERROR_LIBS@ +GPG_ERROR_MT_CFLAGS = @GPG_ERROR_MT_CFLAGS@ +GPG_ERROR_MT_LIBS = @GPG_ERROR_MT_LIBS@ +GRAPHVIZ = @GRAPHVIZ@ +GREP = @GREP@ +HAVE_CXX11 = @HAVE_CXX11@ +HAVE_DOT = @HAVE_DOT@ +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ +INSTALL_PROGRAM = @INSTALL_PROGRAM@ +INSTALL_SCRIPT = @INSTALL_SCRIPT@ +INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@ +LD = @LD@ +LDFLAGS = @LDFLAGS@ +LIBASSUAN_CFLAGS = @LIBASSUAN_CFLAGS@ +LIBASSUAN_CONFIG = @LIBASSUAN_CONFIG@ +LIBASSUAN_LIBS = @LIBASSUAN_LIBS@ +LIBGPGMEPP_LT_AGE = @LIBGPGMEPP_LT_AGE@ +LIBGPGMEPP_LT_CURRENT = @LIBGPGMEPP_LT_CURRENT@ +LIBGPGMEPP_LT_REVISION = @LIBGPGMEPP_LT_REVISION@ +LIBGPGME_LT_AGE = @LIBGPGME_LT_AGE@ +LIBGPGME_LT_CURRENT = @LIBGPGME_LT_CURRENT@ +LIBGPGME_LT_REVISION = @LIBGPGME_LT_REVISION@ +LIBOBJS = @LIBOBJS@ +LIBQGPGME_LT_AGE = @LIBQGPGME_LT_AGE@ +LIBQGPGME_LT_CURRENT = @LIBQGPGME_LT_CURRENT@ +LIBQGPGME_LT_REVISION = @LIBQGPGME_LT_REVISION@ +LIBS = @LIBS@ +LIBTOOL = @LIBTOOL@ +LIPO = @LIPO@ +LN_S = @LN_S@ +LTLIBOBJS = @LTLIBOBJS@ +MAINT = @MAINT@ +MAKEINFO = @MAKEINFO@ +MANIFEST_TOOL = @MANIFEST_TOOL@ +MKDIR_P = @MKDIR_P@ +MOC = @MOC@ +MOC2 = @MOC2@ +NEED__FILE_OFFSET_BITS = @NEED__FILE_OFFSET_BITS@ +NM = @NM@ +NMEDIT = @NMEDIT@ +OBJDUMP = @OBJDUMP@ +OBJEXT = @OBJEXT@ +OTOOL = @OTOOL@ +OTOOL64 = @OTOOL64@ +PACKAGE = @PACKAGE@ +PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@ +PACKAGE_NAME = @PACKAGE_NAME@ +PACKAGE_STRING = @PACKAGE_STRING@ +PACKAGE_TARNAME = @PACKAGE_TARNAME@ +PACKAGE_URL = @PACKAGE_URL@ +PACKAGE_VERSION = @PACKAGE_VERSION@ +PATH_SEPARATOR = @PATH_SEPARATOR@ +PKG_CONFIG = @PKG_CONFIG@ +PYTHON = @PYTHON@ +PYTHONS = @PYTHONS@ +PYTHON_CPPFLAGS = @PYTHON_CPPFLAGS@ +PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@ +PYTHON_EXTRA_LDFLAGS = @PYTHON_EXTRA_LDFLAGS@ +PYTHON_EXTRA_LIBS = @PYTHON_EXTRA_LIBS@ +PYTHON_LDFLAGS = @PYTHON_LDFLAGS@ +PYTHON_PLATFORM = @PYTHON_PLATFORM@ +PYTHON_PREFIX = @PYTHON_PREFIX@ +PYTHON_SITE_PKG = @PYTHON_SITE_PKG@ +PYTHON_VERSION = @PYTHON_VERSION@ +QTCHOOSER = @QTCHOOSER@ +RANLIB = @RANLIB@ +RC = @RC@ +SED = @SED@ +SET_MAKE = @SET_MAKE@ +SHELL = @SHELL@ +STRIP = @STRIP@ +SWIG = @SWIG@ +SWIG_LIB = @SWIG_LIB@ +SYSROOT = @SYSROOT@ +VERSION = @VERSION@ +VERSION_MAJOR = @VERSION_MAJOR@ +VERSION_MICRO = @VERSION_MICRO@ +VERSION_MINOR = @VERSION_MINOR@ +VERSION_NUMBER = @VERSION_NUMBER@ +abs_builddir = @abs_builddir@ +abs_srcdir = @abs_srcdir@ +abs_top_builddir = @abs_top_builddir@ +abs_top_srcdir = @abs_top_srcdir@ +ac_ct_AR = @ac_ct_AR@ +ac_ct_CC = @ac_ct_CC@ +ac_ct_CXX = @ac_ct_CXX@ +ac_ct_DUMPBIN = @ac_ct_DUMPBIN@ +am__include = @am__include@ +am__leading_dot = @am__leading_dot@ +am__quote = @am__quote@ +am__tar = @am__tar@ +am__untar = @am__untar@ +bindir = @bindir@ +build = @build@ +build_alias = @build_alias@ +build_cpu = @build_cpu@ +build_os = @build_os@ +build_vendor = @build_vendor@ +builddir = @builddir@ +datadir = @datadir@ +datarootdir = @datarootdir@ +docdir = @docdir@ +dvidir = @dvidir@ +emacs_local_vars_begin = @emacs_local_vars_begin@ +emacs_local_vars_end = @emacs_local_vars_end@ +emacs_local_vars_read_only = @emacs_local_vars_read_only@ +exec_prefix = @exec_prefix@ +host = @host@ +host_alias = @host_alias@ +host_cpu = @host_cpu@ +host_os = @host_os@ +host_vendor = @host_vendor@ +htmldir = @htmldir@ +includedir = @includedir@ +infodir = @infodir@ +install_sh = @install_sh@ +libdir = @libdir@ +libexecdir = @libexecdir@ +localedir = @localedir@ +localstatedir = @localstatedir@ +mandir = @mandir@ +mkdir_p = @mkdir_p@ +oldincludedir = @oldincludedir@ +pdfdir = @pdfdir@ +pkgpyexecdir = @pkgpyexecdir@ +pkgpythondir = @pkgpythondir@ +prefix = @prefix@ +program_transform_name = @program_transform_name@ +psdir = @psdir@ +pyexecdir = @pyexecdir@ +pythondir = @pythondir@ +sbindir = @sbindir@ +sharedstatedir = @sharedstatedir@ +srcdir = @srcdir@ +sysconfdir = @sysconfdir@ +target_alias = @target_alias@ +top_build_prefix = @top_build_prefix@ +top_builddir = @top_builddir@ +top_srcdir = @top_srcdir@ +SUBDIRS = src BrowserTestExtension DemoExtension +EXTRA_DIST = build_extensions.sh \ + jsdoc.conf \ + jsdoc_index.md \ + .eslintrc.json \ + package.json \ + README \ + unittest_inputvalues.js \ + unittests.js \ + webpack.conf.js \ + webpack.conf_unittests.js + +all: all-recursive + +.SUFFIXES: +$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps) + @for dep in $?; do \ + case '$(am__configure_deps)' in \ + *$$dep*) \ + ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \ + && { if test -f $@; then exit 0; else break; fi; }; \ + exit 1;; \ + esac; \ + done; \ + echo ' cd $(top_srcdir) && $(AUTOMAKE) --gnu lang/js/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --gnu lang/js/Makefile +.PRECIOUS: Makefile +Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status + @case '$?' in \ + *config.status*) \ + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ + *) \ + echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe)'; \ + cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe);; \ + esac; + +$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh + +$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(am__aclocal_m4_deps): + +mostlyclean-libtool: + -rm -f *.lo + +clean-libtool: + -rm -rf .libs _libs + +# This directory's subdirectories are mostly independent; you can cd +# into them and run 'make' without going through this Makefile. +# To change the values of 'make' variables: instead of editing Makefiles, +# (1) if the variable is set in 'config.status', edit 'config.status' +# (which will cause the Makefiles to be regenerated when you run 'make'); +# (2) otherwise, pass the desired values on the 'make' command line. +$(am__recursive_targets): + @fail=; \ + if $(am__make_keepgoing); then \ + failcom='fail=yes'; \ + else \ + failcom='exit 1'; \ + fi; \ + dot_seen=no; \ + target=`echo $@ | sed s/-recursive//`; \ + case "$@" in \ + distclean-* | maintainer-clean-*) list='$(DIST_SUBDIRS)' ;; \ + *) list='$(SUBDIRS)' ;; \ + esac; \ + for subdir in $$list; do \ + echo "Making $$target in $$subdir"; \ + if test "$$subdir" = "."; then \ + dot_seen=yes; \ + local_target="$$target-am"; \ + else \ + local_target="$$target"; \ + fi; \ + ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) $$local_target) \ + || eval $$failcom; \ + done; \ + if test "$$dot_seen" = "no"; then \ + $(MAKE) $(AM_MAKEFLAGS) "$$target-am" || exit 1; \ + fi; test -z "$$fail" + +ID: $(am__tagged_files) + $(am__define_uniq_tagged_files); mkid -fID $$unique +tags: tags-recursive +TAGS: tags + +tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) + set x; \ + here=`pwd`; \ + if ($(ETAGS) --etags-include --version) >/dev/null 2>&1; then \ + include_option=--etags-include; \ + empty_fix=.; \ + else \ + include_option=--include; \ + empty_fix=; \ + fi; \ + list='$(SUBDIRS)'; for subdir in $$list; do \ + if test "$$subdir" = .; then :; else \ + test ! -f $$subdir/TAGS || \ + set "$$@" "$$include_option=$$here/$$subdir/TAGS"; \ + fi; \ + done; \ + $(am__define_uniq_tagged_files); \ + shift; \ + if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \ + test -n "$$unique" || unique=$$empty_fix; \ + if test $$# -gt 0; then \ + $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ + "$$@" $$unique; \ + else \ + $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ + $$unique; \ + fi; \ + fi +ctags: ctags-recursive + +CTAGS: ctags +ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) + $(am__define_uniq_tagged_files); \ + test -z "$(CTAGS_ARGS)$$unique" \ + || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \ + $$unique + +GTAGS: + here=`$(am__cd) $(top_builddir) && pwd` \ + && $(am__cd) $(top_srcdir) \ + && gtags -i $(GTAGS_ARGS) "$$here" +cscopelist: cscopelist-recursive + +cscopelist-am: $(am__tagged_files) + list='$(am__tagged_files)'; \ + case "$(srcdir)" in \ + [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \ + *) sdir=$(subdir)/$(srcdir) ;; \ + esac; \ + for i in $$list; do \ + if test -f "$$i"; then \ + echo "$(subdir)/$$i"; \ + else \ + echo "$$sdir/$$i"; \ + fi; \ + done >> $(top_builddir)/cscope.files + +distclean-tags: + -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags + +distdir: $(DISTFILES) + @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + list='$(DISTFILES)'; \ + dist_files=`for file in $$list; do echo $$file; done | \ + sed -e "s|^$$srcdirstrip/||;t" \ + -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ + case $$dist_files in \ + */*) $(MKDIR_P) `echo "$$dist_files" | \ + sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ + sort -u` ;; \ + esac; \ + for file in $$dist_files; do \ + if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ + if test -d $$d/$$file; then \ + dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ + if test -d "$(distdir)/$$file"; then \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ + cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ + else \ + test -f "$(distdir)/$$file" \ + || cp -p $$d/$$file "$(distdir)/$$file" \ + || exit 1; \ + fi; \ + done + @list='$(DIST_SUBDIRS)'; for subdir in $$list; do \ + if test "$$subdir" = .; then :; else \ + $(am__make_dryrun) \ + || test -d "$(distdir)/$$subdir" \ + || $(MKDIR_P) "$(distdir)/$$subdir" \ + || exit 1; \ + dir1=$$subdir; dir2="$(distdir)/$$subdir"; \ + $(am__relativize); \ + new_distdir=$$reldir; \ + dir1=$$subdir; dir2="$(top_distdir)"; \ + $(am__relativize); \ + new_top_distdir=$$reldir; \ + echo " (cd $$subdir && $(MAKE) $(AM_MAKEFLAGS) top_distdir="$$new_top_distdir" distdir="$$new_distdir" \\"; \ + echo " am__remove_distdir=: am__skip_length_check=: am__skip_mode_fix=: distdir)"; \ + ($(am__cd) $$subdir && \ + $(MAKE) $(AM_MAKEFLAGS) \ + top_distdir="$$new_top_distdir" \ + distdir="$$new_distdir" \ + am__remove_distdir=: \ + am__skip_length_check=: \ + am__skip_mode_fix=: \ + distdir) \ + || exit 1; \ + fi; \ + done +check-am: all-am +check: check-recursive +all-am: Makefile +installdirs: installdirs-recursive +installdirs-am: +install: install-recursive +install-exec: install-exec-recursive +install-data: install-data-recursive +uninstall: uninstall-recursive + +install-am: all-am + @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am + +installcheck: installcheck-recursive +install-strip: + if test -z '$(STRIP)'; then \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + install; \ + else \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \ + fi +mostlyclean-generic: + +clean-generic: + +distclean-generic: + -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES) + -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES) + +maintainer-clean-generic: + @echo "This command is intended for maintainers to use" + @echo "it deletes files that may require special tools to rebuild." +clean: clean-recursive + +clean-am: clean-generic clean-libtool mostlyclean-am + +distclean: distclean-recursive + -rm -f Makefile +distclean-am: clean-am distclean-generic distclean-tags + +dvi: dvi-recursive + +dvi-am: + +html: html-recursive + +html-am: + +info: info-recursive + +info-am: + +install-data-am: + +install-dvi: install-dvi-recursive + +install-dvi-am: + +install-exec-am: + +install-html: install-html-recursive + +install-html-am: + +install-info: install-info-recursive + +install-info-am: + +install-man: + +install-pdf: install-pdf-recursive + +install-pdf-am: + +install-ps: install-ps-recursive + +install-ps-am: + +installcheck-am: + +maintainer-clean: maintainer-clean-recursive + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic + +mostlyclean: mostlyclean-recursive + +mostlyclean-am: mostlyclean-generic mostlyclean-libtool + +pdf: pdf-recursive + +pdf-am: + +ps: ps-recursive + +ps-am: + +uninstall-am: + +.MAKE: $(am__recursive_targets) install-am install-strip + +.PHONY: $(am__recursive_targets) CTAGS GTAGS TAGS all all-am check \ + check-am clean clean-generic clean-libtool cscopelist-am ctags \ + ctags-am distclean distclean-generic distclean-libtool \ + distclean-tags distdir dvi dvi-am html html-am info info-am \ + install install-am install-data install-data-am install-dvi \ + install-dvi-am install-exec install-exec-am install-html \ + install-html-am install-info install-info-am install-man \ + install-pdf install-pdf-am install-ps install-ps-am \ + install-strip installcheck installcheck-am installdirs \ + installdirs-am maintainer-clean maintainer-clean-generic \ + mostlyclean mostlyclean-generic mostlyclean-libtool pdf pdf-am \ + ps ps-am tags tags-am uninstall uninstall-am + + +# Tell versions [3.59,3.63) of GNU make to not export all variables. +# Otherwise a system limit (for SysV at least) may be exceeded. +.NOEXPORT: diff --git a/lang/js/README b/lang/js/README new file mode 100644 index 0000000..fd95cc4 --- /dev/null +++ b/lang/js/README @@ -0,0 +1,116 @@ +gpgme.js - JavaScript for GPGME +------------------------------- +Initially developed for integration with the Mailvelope Web Extension. + +Overview +-------- + +gpgme.js is a javascript library for direct use of GnuPG in browsers. +It interacts with GPGME through nativeMessaging and gpgme-json. + +It is meant to be distributed directly by its downstream users in +their extension package. As such it is not integrated in the +autotools build system. See build instructions below. + + +gpgme-json +---------- + +gpgme-json (see core src/gpgme-json.c) the json to GPGME bridge is +required as native messaging backend for gpgme.js to work. +It needs to be installed and registered as native messaging +backend with the browser. + +See gpgme-mozilla.json and gpgme-chrome.json examples in +the top level doc/examples as example manifests. + +Any web extension using gpgme.js will need to be whitelisted in the manifest +file by its id. + +Distributors are encouraged to create manifest packages for their +distributions. + + +Building gpgme.js +----------------- + +gpgme.js uses webpack, and thus depends on Node.js for building. +All dependencies will be installed (in a local subdirectory) with the command +`npm install`. + +To create a current version of the package, the command is +`npx webpack --config webpack.conf.js`. +If you want a more debuggable (i.e. not minified) build, just change the mode +in webpack.conf.js. + + +Demo and Test WebExtension: +--------------------------- + +The Demo Extension shows simple examples of the usage of gpgme.js. + +The BrowsertestExtension runs more intensive tests (using the mocha and chai +frameworks). Tests from BrowserTestExtension/tests will be run against the +gpgmejs.bundle.js itself. They aim to test the outward facing functionality +and API. + +Unittests as defined in ./unittests.js will be bundled in +gpgmejs_unittests.bundle.js, and test the separate components of gpgme.js, +which mostly are not exported. + +The file `build_extension.sh` may serve as a pointer on how to build and +assemble these two Extensions and their dependencies. It can directly +be used in most linux systems. + +The resulting folders can just be included in the extensions tab of the browser +in questions (extension debug mode needs to be active). For chrome, selecting +the folder is sufficient, for firefox, the manifest.json needs to be selected. +Please note that it is just for demonstration/debug purposes! + +For the Extensions to successfully communicate with gpgme-json, a manifest file +is needed. + +- `~/.config/chromium/NativeMessagingHosts/gpgmejson.json` + +In the browsers' nativeMessaging configuration folder a file 'gpgmejs.json' +is needed, with the following content: + +- For Chrome/Chromium: + ``` + { + "name": "gpgmejson", + "description": "This is a test application for gpgme.js", + "path": "/usr/bin/gpgme-json", + "type": "stdio", + "allowed_origins": ["chrome-extension://ExtensionIdentifier/"] + } + ``` + The usual path for Linux is similar to: + `~/.config/chromium/NativeMessagingHosts/gpgmejson.json` for + For Windows, the path to the manifest needs to be placed in + `HKEY_LOCAL_MACHINE\SOFTWARE\Google\Chrome\NativeMessagingHosts\gpgmejson` + + - For firefox: + ``` + { + "name": "gpgmejson", + "description": "This is a test application for gpgme.js", + "path": "/usr/bin/gpgme-json", + "type": "stdio", + "allowed_extensions": ["ExtensionIdentifier@temporary-addon"] + } + ``` + + The ExtensionIdentifier can be seen as Extension ID on the about:addons page + if addon-debugging is active. In firefox, the temporary addon is removed once + firefox exits, and the identifier will need to be changed more often. + + The manifest for linux is usually placed at: + `~/.mozilla/native-messaging-hosts/gpgmejson.json` + + +Documentation +------------- + +The documentation can be built by jsdoc. It currently uses the command +`./node_modules/.bin/jsdoc -c jsdoc.conf`. diff --git a/lang/js/build_extensions.sh b/lang/js/build_extensions.sh new file mode 100755 index 0000000..91d5479 --- /dev/null +++ b/lang/js/build_extensions.sh @@ -0,0 +1,17 @@ +#/!bin/bash + +npx webpack --config webpack.conf.js +npx webpack --config webpack.conf_unittests.js +mkdir -p BrowserTestExtension/libs +cp node_modules/chai/chai.js \ + node_modules/mocha/mocha.css \ + node_modules/mocha/mocha.js \ + build/gpgmejs.bundle.js \ + build/gpgmejs_unittests.bundle.js BrowserTestExtension/libs +rm -rf build/extensions +mkdir -p build/extensions +zip -r build/extensions/browsertest.zip BrowserTestExtension + +mkdir -p DemoExtension/libs +cp build/gpgmejs.bundle.js DemoExtension/libs +zip -r build/extensions/demoextension.zip DemoExtension diff --git a/lang/js/jsdoc.conf b/lang/js/jsdoc.conf new file mode 100644 index 0000000..976f4e4 --- /dev/null +++ b/lang/js/jsdoc.conf @@ -0,0 +1,24 @@ +{ + "tags": { + "allowUnknownTags": false, + "dictionaries": ["jsdoc"] + }, + "source": { + "include": ["jsdoc_index.md", "./src"], + "includePattern": ".+\\.js(doc|x)?$", + "excludePattern": "(^|\\/|\\\\)_" + }, + "opts":{ + "destination": "./doc/", + "recurse": true + }, + "sourceType": "module", + "plugins": [], + "templates": { + "cleverLinks": false, + "monospaceLinks": false, + "default": { + "outputSourceFiles": true + } + } +}
\ No newline at end of file diff --git a/lang/js/jsdoc_index.md b/lang/js/jsdoc_index.md new file mode 100644 index 0000000..b7371ad --- /dev/null +++ b/lang/js/jsdoc_index.md @@ -0,0 +1,50 @@ +Using gpgme.js +--------------- +At first, make sure that the environment you want to use gpgme.js in has access +and permissions for nativeMessaging, and gpgme-json installed. For details, +see the README. + +The library itself is started via the {@link init} method. This will test the +nativeMessaging connection, and then resolve into an Object offering +the top level API: + +* [encrypt]{@link GpgME#encrypt} +* [decrypt]{@link GpgME#decrypt} +* [sign]{@link GpgME#sign} +* [verify]{@link GpgME#verify} +* [Keyring]{@link GPGME_Keyring} + +``` +gpgmejs.init() + .then(function(GPGME) { + // using GPGME + }, function(error){ + // error handling; + }) +``` + +All methods that require communication with nativeMessaging are asynchronous, +using Promises. Rejections will be instances of {@link GPGME_Error}. + +An exaeption are Keys, which can be initialized in a 'sync' mode, allowing them +to be cached and used synchronously until manually refreshed. + +Keyring and Keys +---------------- +The gnupg keys can be accessed via the [Keyring]{@link GPGME_Keyring}. + +The Keyring offers the methods for accessing information on all Keys known to +gnupg. + +**Due to security constraints, the javascript-binding currently only offers +limited support for secret-Key interaction.** + +The existance of secret Keys is not secret, and those secret Keys can be used +for signing, but Operations that may expose, modify or delete secret Keys are +not supported. + +* [getKeysArmored]{@link GPGME_Keyring#getKeysArmored} +* [getKeys]{@link GPGME_Keyring#getKeys} +* [getDefaultKey]{@link GPGME_Keyring#getDefaultKey} +* [generateKey]{@link GPGME_Keyring#generateKey} +* [deleteKey]{@link GPGME_Keyring#deleteKey} diff --git a/lang/js/package.json b/lang/js/package.json new file mode 100644 index 0000000..54af298 --- /dev/null +++ b/lang/js/package.json @@ -0,0 +1,17 @@ +{ + "name": "gpgmejs", + "version": "0.0.1-dev", + "description": "Javascript part of the GPGME nativeMessaging integration", + "main": "src/index.js", + "private": true, + "keywords": [], + "author": "", + "license": "LGPL-2.1+", + "devDependencies": { + "webpack": "^4.5.0", + "webpack-cli": "^3.0.8", + "chai": "^4.1.2", + "mocha": "^5.1.1", + "jsdoc": "^3.5.5" + } +} diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js new file mode 100644 index 0000000..d43d55f --- /dev/null +++ b/lang/js/src/Connection.js @@ -0,0 +1,320 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +/* global chrome */ + +import { permittedOperations } from './permittedOperations'; +import { gpgme_error } from './Errors'; +import { GPGME_Message, createMessage } from './Message'; +import { decode, atobArray, Utf8ArrayToStr } from './Helpers'; + +/** + * A Connection handles the nativeMessaging interaction via a port. As the + * protocol only allows up to 1MB of message sent from the nativeApp to the + * browser, the connection will stay open until all parts of a communication + * are finished. For a new request, a new port will open, to avoid mixing + * contexts. + * @class + * @private + */ +export class Connection{ + + constructor (){ + this._connection = chrome.runtime.connectNative('gpgmejson'); + } + + /** + * Immediately closes an open port. + */ + disconnect () { + if (this._connection){ + this._connection.disconnect(); + this._connection = null; + } + } + + + /** + * @typedef {Object} backEndDetails + * @property {String} gpgme Version number of gpgme + * @property {Array<Object>} info Further information about the backend + * and the used applications (Example: + * <pre> + * { + * "protocol": "OpenPGP", + * "fname": "/usr/bin/gpg", + * "version": "2.2.6", + * "req_version": "1.4.0", + * "homedir": "default" + * } + * </pre> + */ + + /** + * Retrieves the information about the backend. + * @param {Boolean} details (optional) If set to false, the promise will + * just return if a connection was successful. + * @param {Number} timeout (optional) + * @returns {Promise<backEndDetails>|Promise<Boolean>} Details from the + * backend + * @async + */ + checkConnection (details = true, timeout = 1000){ + if (typeof timeout !== 'number' && timeout <= 0) { + timeout = 1000; + } + const msg = createMessage('version'); + if (details === true) { + return this.post(msg); + } else { + let me = this; + return new Promise(function (resolve) { + Promise.race([ + me.post(msg), + new Promise(function (resolve, reject){ + setTimeout(function (){ + reject(gpgme_error('CONN_TIMEOUT')); + }, timeout); + }) + ]).then(function (){ // success + resolve(true); + }, function (){ // failure + resolve(false); + }); + }); + } + } + + /** + * Sends a {@link GPGME_Message} via the nativeMessaging port. It + * resolves with the completed answer after all parts have been + * received and reassembled, or rejects with an {@link GPGME_Error}. + * + * @param {GPGME_Message} message + * @returns {Promise<*>} The collected answer, depending on the messages' + * operation + * @private + * @async + */ + post (message){ + if (!message || !(message instanceof GPGME_Message)){ + this.disconnect(); + return Promise.reject(gpgme_error( + 'PARAM_WRONG', 'Connection.post')); + } + if (message.isComplete() !== true){ + this.disconnect(); + return Promise.reject(gpgme_error('MSG_INCOMPLETE')); + } + let chunksize = message.chunksize; + const me = this; + return new Promise(function (resolve, reject){ + let answer = new Answer(message); + let listener = function (msg) { + if (!msg){ + me._connection.onMessage.removeListener(listener); + me._connection.disconnect(); + reject(gpgme_error('CONN_EMPTY_GPG_ANSWER')); + } else { + let answer_result = answer.collect(msg); + if (answer_result !== true){ + me._connection.onMessage.removeListener(listener); + me._connection.disconnect(); + reject(answer_result); + } else { + if (msg.more === true){ + me._connection.postMessage({ + 'op': 'getmore', + 'chunksize': chunksize + }); + } else { + me._connection.onMessage.removeListener(listener); + me._connection.disconnect(); + const message = answer.getMessage(); + if (message instanceof Error){ + reject(message); + } else { + resolve(message); + } + } + } + } + }; + me._connection.onMessage.addListener(listener); + if (permittedOperations[message.operation].pinentry){ + return me._connection.postMessage(message.message); + } else { + return Promise.race([ + me._connection.postMessage(message.message), + function (resolve, reject){ + setTimeout(function (){ + me._connection.disconnect(); + reject(gpgme_error('CONN_TIMEOUT')); + }, 5000); + } + ]).then(function (result){ + return result; + }, function (reject){ + if (!(reject instanceof Error)) { + me._connection.disconnect(); + return gpgme_error('GNUPG_ERROR', reject); + } else { + return reject; + } + }); + } + }); + } +} + + +/** + * A class for answer objects, checking and processing the return messages of + * the nativeMessaging communication. + * @private + */ +class Answer{ + + /** + * @param {GPGME_Message} message + */ + constructor (message){ + this._operation = message.operation; + this._expected = message.expected; + this._response_b64 = null; + } + + get operation (){ + return this._operation; + } + + get expected (){ + return this._expected; + } + + /** + * Adds incoming base64 encoded data to the existing response + * @param {*} msg base64 encoded data. + * @returns {Boolean} + * + * @private + */ + collect (msg){ + if (typeof (msg) !== 'object' || !msg.hasOwnProperty('response')) { + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + if (!this._response_b64){ + this._response_b64 = msg.response; + return true; + } else { + this._response_b64 += msg.response; + return true; + } + } + /** + * Decodes and verifies the base64 encoded answer data. Verified against + * {@link permittedOperations}. + * @returns {Object} The readable gpnupg answer + */ + getMessage (){ + if (this._response_b64 === null){ + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + let _decodedResponse = JSON.parse(atob(this._response_b64)); + let _response = { + format: 'ascii' + }; + let messageKeys = Object.keys(_decodedResponse); + let poa = permittedOperations[this.operation].answer; + if (messageKeys.length === 0){ + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + for (let i= 0; i < messageKeys.length; i++){ + let key = messageKeys[i]; + switch (key) { + case 'type': { + if (_decodedResponse.type === 'error'){ + return (gpgme_error('GNUPG_ERROR', + decode(_decodedResponse.msg))); + } else if (poa.type.indexOf(_decodedResponse.type) < 0){ + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + break; + } + case 'base64': { + break; + } + case 'msg': { + if (_decodedResponse.type === 'error'){ + return (gpgme_error('GNUPG_ERROR', _decodedResponse.msg)); + } + break; + } + default: { + let answerType = null; + if (poa.payload && poa.payload.hasOwnProperty(key)){ + answerType = 'p'; + } else if (poa.info && poa.info.hasOwnProperty(key)){ + answerType = 'i'; + } + if (answerType !== 'p' && answerType !== 'i'){ + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + + if (answerType === 'i') { + if ( typeof (_decodedResponse[key]) !== poa.info[key] ){ + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + _response[key] = decode(_decodedResponse[key]); + + } else if (answerType === 'p') { + if (_decodedResponse.base64 === true + && poa.payload[key] === 'string' + ) { + if (this.expected === 'uint8'){ + _response[key] = atobArray(_decodedResponse[key]); + _response.format = 'uint8'; + + } else if (this.expected === 'base64'){ + _response[key] = _decodedResponse[key]; + _response.format = 'base64'; + + } else { // no 'expected' + _response[key] = Utf8ArrayToStr( + atobArray(_decodedResponse[key])); + _response.format = 'string'; + } + } else if (poa.payload[key] === 'string') { + _response[key] = _decodedResponse[key]; + } else { + // fallthrough, should not be reached + // (payload is always string) + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + } + break; + } } + } + return _response; + } +} diff --git a/lang/js/src/Errors.js b/lang/js/src/Errors.js new file mode 100644 index 0000000..2f66c83 --- /dev/null +++ b/lang/js/src/Errors.js @@ -0,0 +1,177 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +/** + * Listing of all possible error codes and messages of a {@link GPGME_Error}. + */ +export const err_list = { + // Connection + 'CONN_NO_CONNECT': { + msg:'Connection with the nativeMessaging host could not be' + + ' established.', + type: 'error' + }, + 'CONN_EMPTY_GPG_ANSWER':{ + msg: 'The nativeMessaging answer was empty.', + type: 'error' + }, + 'CONN_TIMEOUT': { + msg: 'A connection timeout was exceeded.', + type: 'error' + }, + 'CONN_UNEXPECTED_ANSWER': { + msg: 'The answer from gnupg was not as expected.', + type: 'error' + }, + 'CONN_ALREADY_CONNECTED':{ + msg: 'A connection was already established.', + type: 'warning' + }, + // Message/Data + 'MSG_INCOMPLETE': { + msg: 'The Message did not match the minimum requirements for' + + ' the interaction.', + type: 'error' + }, + 'MSG_EMPTY' : { + msg: 'The Message is empty.', + type: 'error' + }, + 'MSG_WRONG_OP': { + msg: 'The operation requested could not be found', + type: 'error' + }, + 'MSG_NO_KEYS' : { + msg: 'There were no valid keys provided.', + type: 'warning' + }, + 'MSG_NOT_A_FPR': { + msg: 'The String is not an accepted fingerprint', + type: 'warning' + }, + 'KEY_INVALID': { + msg:'Key object is invalid', + type: 'error' + }, + 'KEY_NOKEY': { + msg:'This key does not exist in GPG', + type: 'error' + }, + 'KEY_NO_INIT': { + msg:'This property has not been retrieved yet from GPG', + type: 'error' + }, + 'KEY_ASYNC_ONLY': { + msg: 'This property cannot be used in synchronous calls', + type: 'error' + }, + 'KEY_NO_DEFAULT': { + msg:'A default key could not be established. Please check yout gpg ' + + 'configuration', + type: 'error' + }, + 'SIG_WRONG': { + msg:'A malformed signature was created', + type: 'error' + }, + 'SIG_NO_SIGS': { + msg:'There were no signatures found', + type: 'error' + }, + // generic + 'PARAM_WRONG':{ + msg: 'Invalid parameter was found', + type: 'error' + }, + 'DECODE_FAIL': { + msg: 'Decoding failed due to unexpected data', + type: 'error' + }, + 'PARAM_IGNORED': { + msg: 'An parameter was set that has no effect in gpgmejs', + type: 'warning' + }, + 'GENERIC_ERROR': { + msg: 'Unspecified error', + type: 'error' + } +}; + +/** + * Checks the given error code and returns an {@link GPGME_Error} error object + * with some information about meaning and origin + * @param {String} code Error code as defined in {@link err_list}. + * @param {String} info Possible additional error message to pass through. + * Currently used for errors sent as answer by gnupg via a native Message port + * @returns {GPGME_Error} + */ +export function gpgme_error (code = 'GENERIC_ERROR', info){ + if (err_list.hasOwnProperty(code)){ + if (err_list[code].type === 'error'){ + return new GPGME_Error(code); + } + if (err_list[code].type === 'warning'){ + // eslint-disable-next-line no-console + // console.warn(code + ': ' + err_list[code].msg); + } + return null; + } else if (code === 'GNUPG_ERROR'){ + return new GPGME_Error(code, info); + } + else { + return new GPGME_Error('GENERIC_ERROR'); + } +} + +/** + * An error class with additional info about the origin of the error, as string + * It is created by {@link gpgme_error}, and its' codes are defined in + * {@link err_list}. + * + * @property {String} code Short description of origin and type of the error + * @property {String} msg Additional info + * @protected + * @class + * @extends Error + */ +class GPGME_Error extends Error{ + constructor (code = 'GENERIC_ERROR', msg=''){ + + if (code === 'GNUPG_ERROR' && typeof (msg) === 'string'){ + super(msg); + } else if (err_list.hasOwnProperty(code)){ + if (msg){ + super(err_list[code].msg + '--' + msg); + } else { + super(err_list[code].msg); + } + } else { + super(err_list['GENERIC_ERROR'].msg); + } + this._code = code; + } + + get code (){ + return this._code; + } +}
\ No newline at end of file diff --git a/lang/js/src/Helpers.js b/lang/js/src/Helpers.js new file mode 100644 index 0000000..0b41852 --- /dev/null +++ b/lang/js/src/Helpers.js @@ -0,0 +1,219 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +import { gpgme_error } from './Errors'; + +/** + * Helper function that tries to return an array of fingerprints, either from + * input fingerprints or from Key objects (openpgp Keys or GPGME_Keys are both + * accepted). + * + * @param {Object | Object[] | String | String[] } input + * @returns {String[]} Array of fingerprints, or an empty array + */ +export function toKeyIdArray (input){ + if (!input){ + return []; + } + if (!Array.isArray(input)){ + input = [input]; + } + let result = []; + for (let i=0; i < input.length; i++){ + if (typeof (input[i]) === 'string'){ + if (isFingerprint(input[i]) === true){ + result.push(input[i]); + } else { + // MSG_NOT_A_FPR is just a console warning if warning enabled + // in src/Errors.js + gpgme_error('MSG_NOT_A_FPR'); + } + } else if (typeof (input[i]) === 'object'){ + let fpr = ''; + if (input[i].fingerprint !== undefined){ + fpr = input[i].fingerprint; + } else if (input[i].hasOwnProperty('primaryKey') && + input[i].primaryKey.hasOwnProperty('getFingerprint')){ + fpr = input[i].primaryKey.getFingerprint(); + } + if (isFingerprint(fpr) === true){ + result.push(fpr); + } else { + gpgme_error('MSG_NOT_A_FPR'); + } + } else { + return gpgme_error('PARAM_WRONG'); + } + } + if (result.length === 0){ + return []; + } else { + return result; + } +} + +/** + * Check if values are valid hexadecimal values of a specified length + * @param {String} key input value. + * @param {int} len the expected length of the value + * @returns {Boolean} true if value passes test + * @private + */ +function hextest (key, len){ + if (!key || typeof (key) !== 'string'){ + return false; + } + if (key.length !== len){ + return false; + } + let regexp= /^[0-9a-fA-F]*$/i; + return regexp.test(key); +} + +/** + * Checks if the input is a valid Fingerprint + * (Hex string with a length of 40 characters) + * @param {String} value to check + * @returns {Boolean} true if value passes test + */ +export function isFingerprint (value){ + return hextest(value, 40); +} + +/** + * check if the input is a valid gnupg long ID (Hex string with a length of 16 + * characters) + * @param {String} value to check + * @returns {Boolean} true if value passes test + */ +export function isLongId (value){ + return hextest(value, 16); +} + +/** + * Recursively decodes input (utf8) to output (utf-16; javascript) strings. + * @param {Object | Array | String} property + * @private + */ +export function decode (property){ + if (typeof property === 'string'){ + try { + return decodeURIComponent(escape(unescape(property))); + } + catch (error){ + if (error instanceof URIError) { + return property; + } + } + } else if (Array.isArray(property)){ + let res = []; + for (let arr=0; arr < property.length; arr++){ + res.push(decode(property[arr])); + } + return res; + } else if (typeof property === 'object'){ + const keys = Object.keys(property); + if (keys.length){ + let res = {}; + for (let k=0; k < keys.length; k++ ){ + res[keys[k]] = decode(property[keys[k]]); + } + return res; + } + return property; + } + return property; +} + +/** + * Turns a base64 encoded string into an uint8 array + * adapted from https://gist.github.com/borismus/1032746 + * @param {String} base64 encoded String + * @returns {Uint8Array} + * @private + */ +export function atobArray (base64) { + if (typeof (base64) !== 'string'){ + throw gpgme_error('DECODE_FAIL'); + } + const raw = window.atob(base64); + const rawLength = raw.length; + let array = new Uint8Array(new ArrayBuffer(rawLength)); + for (let i = 0; i < rawLength; i++) { + array[i] = raw.charCodeAt(i); + } + return array; +} + +/** + * Turns a Uint8Array into an utf8-String + * <pre> + * Taken and slightly adapted from + * http://www.onicos.com/staff/iz/amuse/javascript/expert/utf.txt + * (original header: + * utf.js - UTF-8 <=> UTF-16 convertion + * + * Copyright (C) 1999 Masanao Izumo <iz@onicos.co.jp> + * Version: 1.0 + * LastModified: Dec 25 1999 + * This library is free. You can redistribute it and/or modify it. + * ) + * </pre> + * @param {*} array Uint8Array + * @returns {String} + * @private + */ +export function Utf8ArrayToStr (array) { + let out, i, len, c, char2, char3; + out = ''; + len = array.length; + i = 0; + if (array instanceof Uint8Array === false){ + throw gpgme_error('DECODE_FAIL'); + } + while (i < len) { + c = array[i++]; + switch (c >> 4) { + case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: + // 0xxxxxxx + out += String.fromCharCode(c); + break; + case 12: case 13: + // 110x xxxx 10xx xxxx + char2 = array[i++]; + out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F)); + break; + case 14: + // 1110 xxxx 10xx xxxx 10xx xxxx + char2 = array[i++]; + char3 = array[i++]; + out += String.fromCharCode(((c & 0x0F) << 12) | + ((char2 & 0x3F) << 6) | + ((char3 & 0x3F) << 0)); + break; + default: + break; + } + } + return out; +} diff --git a/lang/js/src/Key.js b/lang/js/src/Key.js new file mode 100644 index 0000000..7f0554c --- /dev/null +++ b/lang/js/src/Key.js @@ -0,0 +1,711 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + + +import { isFingerprint, isLongId } from './Helpers'; +import { gpgme_error } from './Errors'; +import { createMessage } from './Message'; + +/** + * Validates the given fingerprint and creates a new {@link GPGME_Key} + * @param {String} fingerprint + * @param {Boolean} async If True, Key properties (except fingerprint) will be + * queried from gnupg on each call, making the operation up-to-date, the + * answers will be Promises, and the performance will likely suffer + * @param {Object} data additional initial properties this Key will have. Needs + * a full object as delivered by gpgme-json + * @returns {Object} The verified and updated data + */ +export function createKey (fingerprint, async = false, data){ + if (!isFingerprint(fingerprint) || typeof (async) !== 'boolean'){ + throw gpgme_error('PARAM_WRONG'); + } + if (data !== undefined){ + data = validateKeyData(fingerprint, data); + } + if (data instanceof Error){ + throw gpgme_error('KEY_INVALID'); + } else { + return new GPGME_Key(fingerprint, async, data); + } +} + +/** + * Represents the Keys as stored in the gnupg backend. A key is defined by a + * fingerprint. + * A key cannot be directly created via the new operator, please use + * {@link createKey} instead. + * A GPGME_Key object allows to query almost all information defined in gpgme + * Keys. It offers two modes, async: true/false. In async mode, Key properties + * with the exception of the fingerprint will be queried from gnupg on each + * call, making the operation up-to-date, the answers will be Promises, and + * the performance will likely suffer. In Sync modes, all information except + * for the armored Key export will be cached and can be refreshed by + * [refreshKey]{@link GPGME_Key#refreshKey}. + * + * <pre> + * see also: + * {@link GPGME_UserId} user Id objects + * {@link GPGME_Subkey} subKey objects + * </pre> + * For other Key properteis, refer to {@link validKeyProperties}, + * and to the [gpgme documentation]{@link https://www.gnupg.org/documentation/manuals/gpgme/Key-objects.html} + * for meanings and further details. + * + * @class + */ +class GPGME_Key { + + constructor (fingerprint, async, data){ + + /** + * @property {Boolean} _async If true, the Key was initialized without + * cached data + */ + this._async = async; + + this._data = { fingerprint: fingerprint.toUpperCase() }; + if (data !== undefined + && data.fingerprint.toUpperCase() === this._data.fingerprint + ) { + this._data = data; + } + } + + /** + * Query any property of the Key listed in {@link validKeyProperties} + * @param {String} property property to be retreived + * @returns {Boolean| String | Date | Array | Object} + * @returns {Promise<Boolean| String | Date | Array | Object>} (if in async + * mode) + * <pre> + * Returns the value of the property requested. If the Key is set to async, + * the value will be fetched from gnupg and resolved as a Promise. If Key + * is not async, the armored property is not available (it can still be + * retrieved asynchronously by [getArmor]{@link GPGME_Key#getArmor}) + */ + get (property) { + if (this._async === true) { + switch (property){ + case 'armored': + return this.getArmor(); + case 'hasSecret': + return this.getGnupgSecretState(); + default: + return getGnupgState(this.fingerprint, property); + } + } else { + if (property === 'armored') { + throw gpgme_error('KEY_ASYNC_ONLY'); + } + // eslint-disable-next-line no-use-before-define + if (!validKeyProperties.hasOwnProperty(property)){ + throw gpgme_error('PARAM_WRONG'); + } else { + return (this._data[property]); + } + } + } + + /** + * Reloads the Key information from gnupg. This is only useful if the Key + * use the GPGME_Keys cached. Note that this is a performance hungry + * operation. If you desire more than a few refreshs, it may be + * advisable to run [Keyring.getKeys]{@link Keyring#getKeys} instead. + * @returns {Promise<GPGME_Key>} + * @async + */ + refreshKey () { + let me = this; + return new Promise(function (resolve, reject) { + if (!me._data.fingerprint){ + reject(gpgme_error('KEY_INVALID')); + } + let msg = createMessage('keylist'); + msg.setParameter('sigs', true); + msg.setParameter('keys', me._data.fingerprint); + msg.post().then(function (result){ + if (result.keys.length === 1){ + const newdata = validateKeyData( + me._data.fingerprint, result.keys[0]); + if (newdata instanceof Error){ + reject(gpgme_error('KEY_INVALID')); + } else { + me._data = newdata; + me.getGnupgSecretState().then(function (){ + me.getArmor().then(function (){ + resolve(me); + }, function (error){ + reject(error); + }); + }, function (error){ + reject(error); + }); + } + } else { + reject(gpgme_error('KEY_NOKEY')); + } + }, function (error) { + reject(gpgme_error('GNUPG_ERROR'), error); + }); + }); + } + + /** + * Query the armored block of the Key directly from gnupg. Please note + * that this will not get you any export of the secret/private parts of + * a Key + * @returns {Promise<String>} + * @async + */ + getArmor () { + const me = this; + return new Promise(function (resolve, reject) { + if (!me._data.fingerprint){ + reject(gpgme_error('KEY_INVALID')); + } + let msg = createMessage('export'); + msg.setParameter('armor', true); + msg.setParameter('keys', me._data.fingerprint); + msg.post().then(function (result){ + resolve(result.data); + }, function (error){ + reject(error); + }); + }); + } + + /** + * Find out if the Key is part of a Key pair including public and + * private key(s). If you want this information about more than a few + * Keys in synchronous mode, it may be advisable to run + * [Keyring.getKeys]{@link Keyring#getKeys} instead, as it performs faster + * in bulk querying. + * @returns {Promise<Boolean>} True if a private Key is available in the + * gnupg Keyring. + * @async + */ + getGnupgSecretState (){ + const me = this; + return new Promise(function (resolve, reject) { + if (!me._data.fingerprint){ + reject(gpgme_error('KEY_INVALID')); + } else { + let msg = createMessage('keylist'); + msg.setParameter('keys', me._data.fingerprint); + msg.setParameter('secret', true); + msg.post().then(function (result){ + me._data.hasSecret = null; + if ( + result.keys && + result.keys.length === 1 && + result.keys[0].secret === true + ) { + me._data.hasSecret = true; + resolve(true); + } else { + me._data.hasSecret = false; + resolve(false); + } + }, function (error){ + reject(error); + }); + } + }); + } + + /** + * Deletes the (public) Key from the GPG Keyring. Note that a deletion + * of a secret key is not supported by the native backend, and gnupg will + * refuse to delete a Key if there is still a secret/private Key present + * to that public Key + * @returns {Promise<Boolean>} Success if key was deleted. + */ + delete (){ + const me = this; + return new Promise(function (resolve, reject){ + if (!me._data.fingerprint){ + reject(gpgme_error('KEY_INVALID')); + } + let msg = createMessage('delete'); + msg.setParameter('key', me._data.fingerprint); + msg.post().then(function (result){ + resolve(result.success); + }, function (error){ + reject(error); + }); + }); + } + + /** + * @returns {String} The fingerprint defining this Key. Convenience getter + */ + get fingerprint (){ + return this._data.fingerprint; + } +} + +/** + * Representing a subkey of a Key. See {@link validSubKeyProperties} for + * possible properties. + * @class + * @protected + */ +class GPGME_Subkey { + + /** + * Initializes with the json data sent by gpgme-json + * @param {Object} data + * @private + */ + constructor (data){ + this._data = {}; + let keys = Object.keys(data); + const me = this; + + /** + * Validates a subkey property against {@link validSubKeyProperties} and + * sets it if validation is successful + * @param {String} property + * @param {*} value + * @param private + */ + const setProperty = function (property, value){ + // eslint-disable-next-line no-use-before-define + if (validSubKeyProperties.hasOwnProperty(property)){ + // eslint-disable-next-line no-use-before-define + if (validSubKeyProperties[property](value) === true) { + if (property === 'timestamp' || property === 'expires'){ + me._data[property] = new Date(value * 1000); + } else { + me._data[property] = value; + } + } + } + }; + for (let i=0; i< keys.length; i++) { + setProperty(keys[i], data[keys[i]]); + } + } + + /** + * Fetches any information about this subkey + * @param {String} property Information to request + * @returns {String | Number | Date} + */ + get (property) { + if (this._data.hasOwnProperty(property)){ + return (this._data[property]); + } + } + +} + +/** + * Representing user attributes associated with a Key or subkey. See + * {@link validUserIdProperties} for possible properties. + * @class + * @protected + */ +class GPGME_UserId { + + /** + * Initializes with the json data sent by gpgme-json + * @param {Object} data + * @private + */ + constructor (data){ + this._data = {}; + const me = this; + let keys = Object.keys(data); + const setProperty = function (property, value){ + // eslint-disable-next-line no-use-before-define + if (validUserIdProperties.hasOwnProperty(property)){ + // eslint-disable-next-line no-use-before-define + if (validUserIdProperties[property](value) === true) { + if (property === 'last_update'){ + me._data[property] = new Date(value*1000); + } else { + me._data[property] = value; + } + } + } + }; + for (let i=0; i< keys.length; i++) { + setProperty(keys[i], data[keys[i]]); + } + } + + /** + * Fetches information about the user + * @param {String} property Information to request + * @returns {String | Number} + */ + get (property) { + if (this._data.hasOwnProperty(property)){ + return (this._data[property]); + } + } + +} + +/** + * Validation definition for userIds. Each valid userId property is represented + * as a key- Value pair, with their value being a validation function to check + * against + * @protected + * @const + */ +const validUserIdProperties = { + 'revoked': function (value){ + return typeof (value) === 'boolean'; + }, + 'invalid': function (value){ + return typeof (value) === 'boolean'; + }, + 'uid': function (value){ + if (typeof (value) === 'string' || value === ''){ + return true; + } + return false; + }, + 'validity': function (value){ + if (typeof (value) === 'string'){ + return true; + } + return false; + }, + 'name': function (value){ + if (typeof (value) === 'string' || value === ''){ + return true; + } + return false; + }, + 'email': function (value){ + if (typeof (value) === 'string' || value === ''){ + return true; + } + return false; + }, + 'address': function (value){ + if (typeof (value) === 'string' || value === ''){ + return true; + } + return false; + }, + 'comment': function (value){ + if (typeof (value) === 'string' || value === ''){ + return true; + } + return false; + }, + 'origin': function (value){ + return Number.isInteger(value); + }, + 'last_update': function (value){ + return Number.isInteger(value); + } +}; + +/** + * Validation definition for subKeys. Each valid userId property is represented + * as a key-value pair, with the value being a validation function + * @protected + * @const + */ +const validSubKeyProperties = { + 'invalid': function (value){ + return typeof (value) === 'boolean'; + }, + 'can_encrypt': function (value){ + return typeof (value) === 'boolean'; + }, + 'can_sign': function (value){ + return typeof (value) === 'boolean'; + }, + 'can_certify': function (value){ + return typeof (value) === 'boolean'; + }, + 'can_authenticate': function (value){ + return typeof (value) === 'boolean'; + }, + 'secret': function (value){ + return typeof (value) === 'boolean'; + }, + 'is_qualified': function (value){ + return typeof (value) === 'boolean'; + }, + 'is_cardkey': function (value){ + return typeof (value) === 'boolean'; + }, + 'is_de_vs': function (value){ + return typeof (value) === 'boolean'; + }, + 'pubkey_algo_name': function (value){ + return typeof (value) === 'string'; + // TODO: check against list of known?[''] + }, + 'pubkey_algo_string': function (value){ + return typeof (value) === 'string'; + // TODO: check against list of known?[''] + }, + 'keyid': function (value){ + return isLongId(value); + }, + 'pubkey_algo': function (value) { + return (Number.isInteger(value) && value >= 0); + }, + 'length': function (value){ + return (Number.isInteger(value) && value > 0); + }, + 'timestamp': function (value){ + return (Number.isInteger(value) && value > 0); + }, + 'expires': function (value){ + return (Number.isInteger(value) && value > 0); + } +}; + +/** + * Validation definition for Keys. Each valid Key property is represented + * as a key-value pair, with their value being a validation function. For + * details on the meanings, please refer to the gpgme documentation + * https://www.gnupg.org/documentation/manuals/gpgme/Key-objects.html#Key-objects + * @param {String} fingerprint + * @param {Boolean} revoked + * @param {Boolean} expired + * @param {Boolean} disabled + * @param {Boolean} invalid + * @param {Boolean} can_encrypt + * @param {Boolean} can_sign + * @param {Boolean} can_certify + * @param {Boolean} can_authenticate + * @param {Boolean} secret + * @param {Boolean}is_qualified + * @param {String} protocol + * @param {String} issuer_serial + * @param {String} issuer_name + * @param {Boolean} chain_id + * @param {String} owner_trust + * @param {Date} last_update + * @param {String} origin + * @param {Array<GPGME_Subkey>} subkeys + * @param {Array<GPGME_UserId>} userids + * @param {Array<String>} tofu + * @param {Boolean} hasSecret + * @protected + * @const + */ +const validKeyProperties = { + 'fingerprint': function (value){ + return isFingerprint(value); + }, + 'revoked': function (value){ + return typeof (value) === 'boolean'; + }, + 'expired': function (value){ + return typeof (value) === 'boolean'; + }, + 'disabled': function (value){ + return typeof (value) === 'boolean'; + }, + 'invalid': function (value){ + return typeof (value) === 'boolean'; + }, + 'can_encrypt': function (value){ + return typeof (value) === 'boolean'; + }, + 'can_sign': function (value){ + return typeof (value) === 'boolean'; + }, + 'can_certify': function (value){ + return typeof (value) === 'boolean'; + }, + 'can_authenticate': function (value){ + return typeof (value) === 'boolean'; + }, + 'secret': function (value){ + return typeof (value) === 'boolean'; + }, + 'is_qualified': function (value){ + return typeof (value) === 'boolean'; + }, + 'protocol': function (value){ + return typeof (value) === 'string'; + // TODO check for implemented ones + }, + 'issuer_serial': function (value){ + return typeof (value) === 'string'; + }, + 'issuer_name': function (value){ + return typeof (value) === 'string'; + }, + 'chain_id': function (value){ + return typeof (value) === 'string'; + }, + 'owner_trust': function (value){ + return typeof (value) === 'string'; + }, + 'last_update': function (value){ + return (Number.isInteger(value)); + // TODO undefined/null possible? + }, + 'origin': function (value){ + return (Number.isInteger(value)); + }, + 'subkeys': function (value){ + return (Array.isArray(value)); + }, + 'userids': function (value){ + return (Array.isArray(value)); + }, + 'tofu': function (value){ + return (Array.isArray(value)); + }, + 'hasSecret': function (value){ + return typeof (value) === 'boolean'; + } + +}; + +/** +* sets the Key data in bulk. It can only be used from inside a Key, either +* during construction or on a refresh callback. +* @param {Object} key the original internal key data. +* @param {Object} data Bulk set the data for this key, with an Object structure +* as sent by gpgme-json. +* @returns {Object|GPGME_Error} the changed data after values have been set, +* an error if something went wrong. +* @private +*/ +function validateKeyData (fingerprint, data){ + const key = {}; + if (!fingerprint || typeof (data) !== 'object' || !data.fingerprint + || fingerprint !== data.fingerprint.toUpperCase() + ){ + return gpgme_error('KEY_INVALID'); + } + let props = Object.keys(data); + for (let i=0; i< props.length; i++){ + if (!validKeyProperties.hasOwnProperty(props[i])){ + return gpgme_error('KEY_INVALID'); + } + // running the defined validation function + if (validKeyProperties[props[i]](data[props[i]]) !== true ){ + return gpgme_error('KEY_INVALID'); + } + switch (props[i]){ + case 'subkeys': + key.subkeys = []; + for (let i=0; i< data.subkeys.length; i++) { + key.subkeys.push( + new GPGME_Subkey(data.subkeys[i])); + } + break; + case 'userids': + key.userids = []; + for (let i=0; i< data.userids.length; i++) { + key.userids.push( + new GPGME_UserId(data.userids[i])); + } + break; + case 'last_update': + key[props[i]] = new Date( data[props[i]] * 1000 ); + break; + default: + key[props[i]] = data[props[i]]; + } + } + return key; +} + +/** + * Fetches and sets properties from gnupg + * @param {String} fingerprint + * @param {String} property to search for. + * @private + * @async + */ +function getGnupgState (fingerprint, property){ + return new Promise(function (resolve, reject) { + if (!isFingerprint(fingerprint)) { + reject(gpgme_error('KEY_INVALID')); + } else { + let msg = createMessage('keylist'); + msg.setParameter('keys', fingerprint); + msg.post().then(function (res){ + if (!res.keys || res.keys.length !== 1){ + reject(gpgme_error('KEY_INVALID')); + } else { + const key = res.keys[0]; + let result; + switch (property){ + case 'subkeys': + result = []; + if (key.subkeys.length){ + for (let i=0; i < key.subkeys.length; i++) { + result.push( + new GPGME_Subkey(key.subkeys[i])); + } + } + resolve(result); + break; + case 'userids': + result = []; + if (key.userids.length){ + for (let i=0; i< key.userids.length; i++) { + result.push( + new GPGME_UserId(key.userids[i])); + } + } + resolve(result); + break; + case 'last_update': + if (key.last_update === undefined){ + reject(gpgme_error('CONN_UNEXPECTED_ANSWER')); + } else if (key.last_update !== null){ + resolve(new Date( key.last_update * 1000)); + } else { + resolve(null); + } + break; + default: + if (!validKeyProperties.hasOwnProperty(property)){ + reject(gpgme_error('PARAM_WRONG')); + } else { + if (key.hasOwnProperty(property)){ + resolve(key[property]); + } else { + reject(gpgme_error( + 'CONN_UNEXPECTED_ANSWER')); + } + } + break; + } + } + }, function (error){ + reject(gpgme_error(error)); + }); + } + }); +}
\ No newline at end of file diff --git a/lang/js/src/Keyring.js b/lang/js/src/Keyring.js new file mode 100644 index 0000000..2071c6d --- /dev/null +++ b/lang/js/src/Keyring.js @@ -0,0 +1,439 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + + +import { createMessage } from './Message'; +import { createKey } from './Key'; +import { isFingerprint } from './Helpers'; +import { gpgme_error } from './Errors'; + +/** + * This class offers access to the gnupg keyring + */ +export class GPGME_Keyring { + + /** + * Queries Keys (all Keys or a subset) from gnupg. + * + * @param {Object} options: + * @param {String | Array<String>} options.pattern (optional) A pattern to + * search for in userIds or KeyIds. + * @param {Boolean} options.prepare_sync (optional) if set to true, most + * data (with the exception of armored Key blocks) will be cached for the + * Keys. This enables direct, synchronous use of these properties for + * all keys. It does not check for changes on the backend. The cached + * information can be updated with the {@link Key.refresh} method. + * @param {Boolean} options.search (optional) retrieve Keys from external + * servers with the method(s) defined in gnupg (e.g. WKD/HKP lookup) + * @returns {Promise<GPGME_Key[]>} + * @static + * @async + */ + getKeys ({ pattern, prepare_sync = false, search = false } = {}){ + if (typeof arguments[0] !== 'object') { + return Promise.reject(gpgme_error('PARAM_WRONG')); + } + if (arguments.length && typeof arguments[0] !== 'object') { + return Promise.reject(gpgme_error('PARAM_WRONG')); + } + return new Promise(function (resolve, reject) { + let msg = createMessage('keylist'); + if (pattern) { + msg.setParameter('keys', pattern); + } + msg.setParameter('sigs', true); + if (search === true){ + msg.setParameter('locate', true); + } + msg.post().then(function (result){ + let resultset = []; + if (result.keys.length === 0){ + resolve([]); + } else { + let secondrequest; + if (prepare_sync === true) { + secondrequest = function () { + let msg2 = createMessage('keylist'); + if (pattern){ + msg2.setParameter('keys', pattern); + } + msg2.setParameter('secret', true); + return msg2.post(); + }; + } else { + secondrequest = function () { + return Promise.resolve(true); + }; + } + secondrequest().then(function (answer) { + for (let i=0; i < result.keys.length; i++){ + if (prepare_sync === true){ + if (answer && answer.keys) { + for (let j=0; + j < answer.keys.length; j++ ){ + const a = answer.keys[j]; + const b = result.keys[i]; + if ( + a.fingerprint === b.fingerprint + ) { + if (a.secret === true){ + b.hasSecret = true; + } else { + b.hasSecret = false; + } + break; + } + } + } + } + let k = createKey(result.keys[i].fingerprint, + !prepare_sync, result.keys[i]); + resultset.push(k); + } + resolve(resultset); + }, function (error){ + reject(error); + }); + } + }); + }); + } + + /** + * @typedef {Object} exportResult The result of a getKeysArmored + * operation. + * @property {String} armored The public Key(s) as armored block. Note + * that the result is one armored block, and not a block per key. + * @property {Array<String>} secret_fprs (optional) list of + * fingerprints for those Keys that also have a secret Key available in + * gnupg. The secret key will not be exported, but the fingerprint can + * be used in operations needing a secret key. + */ + + /** + * Fetches the armored public Key blocks for all Keys matching the + * pattern (if no pattern is given, fetches all keys known to gnupg). + * @param {Object} options (optional) + * @param {String|Array<String>} options.pattern The Pattern to + * search for + * @param {Boolean} options.with_secret_fpr also return a list of + * fingerprints for the keys that have a secret key available + * @returns {Promise<exportResult>} Object containing the + * armored Key(s) and additional information. + * @static + * @async + */ + getKeysArmored ({ pattern, with_secret_fpr }) { + return new Promise(function (resolve, reject) { + let msg = createMessage('export'); + msg.setParameter('armor', true); + if (with_secret_fpr === true) { + msg.setParameter('with-sec-fprs', true); + } + if (pattern){ + msg.setParameter('keys', pattern); + } + msg.post().then(function (answer){ + const result = { armored: answer.data }; + if (with_secret_fpr === true){ + if (answer.hasOwnProperty('sec-fprs')){ + result.secret_fprs = answer['sec-fprs']; + } else { + result.secret_fprs = []; + } + } + resolve(result); + }, function (error){ + reject(error); + }); + }); + } + + /** + * Returns the Key used by default in gnupg. + * (a.k.a. 'primary Key or 'main key'). + * It looks up the gpg configuration if set, or the first key that + * contains a secret key. + * + * @returns {Promise<GPGME_Key>} + * @async + * @static + */ + getDefaultKey (prepare_sync = false) { + let me = this; + return new Promise(function (resolve, reject){ + let msg = createMessage('config_opt'); + msg.setParameter('component', 'gpg'); + msg.setParameter('option', 'default-key'); + msg.post().then(function (resp){ + if (resp.option !== undefined + && resp.option.hasOwnProperty('value') + && resp.option.value.length === 1 + && resp.option.value[0].hasOwnProperty('string') + && typeof (resp.option.value[0].string) === 'string'){ + me.getKeys({ pattern: resp.option.value[0].string, + prepare_sync: true }).then( + function (keys){ + if (keys.length === 1){ + resolve(keys[0]); + } else { + reject(gpgme_error('KEY_NO_DEFAULT')); + } + }, function (error){ + reject(error); + }); + } else { + let msg = createMessage('keylist'); + msg.setParameter('secret', true); + msg.post().then(function (result){ + if (result.keys.length === 0){ + reject(gpgme_error('KEY_NO_DEFAULT')); + } else { + for (let i=0; i< result.keys.length; i++ ) { + if ( + result.keys[i].invalid === false && + result.keys[i].expired === false && + result.keys[i].revoked === false && + result.keys[i].can_sign === true + ) { + let k = createKey( + result.keys[i].fingerprint, + !prepare_sync, + result.keys[i]); + resolve(k); + break; + } else if (i === result.keys.length - 1){ + reject(gpgme_error('KEY_NO_DEFAULT')); + } + } + } + }, function (error){ + reject(error); + }); + } + }, function (error){ + reject(error); + }); + }); + } + + /** + * @typedef {Object} importResult The result of a Key update + * @property {Object} summary Numerical summary of the result. See the + * feedbackValues variable for available Keys values and the gnupg + * documentation. + * https://www.gnupg.org/documentation/manuals/gpgme/Importing-Keys.html + * for details on their meaning. + * @property {Array<importedKeyResult>} Keys Array of Object containing + * GPGME_Keys with additional import information + * + */ + + /** + * @typedef {Object} importedKeyResult + * @property {GPGME_Key} key The resulting key + * @property {String} status: + * 'nochange' if the Key was not changed, + * 'newkey' if the Key was imported in gpg, and did not exist + * previously, + * 'change' if the key existed, but details were updated. For details, + * Key.changes is available. + * @property {Boolean} changes.userId Changes in userIds + * @property {Boolean} changes.signature Changes in signatures + * @property {Boolean} changes.subkey Changes in subkeys + */ + + /** + * Import an armored Key block into gnupg. Note that this currently + * will not succeed on private Key blocks. + * @param {String} armored Armored Key block of the Key(s) to be + * imported into gnupg + * @param {Boolean} prepare_sync prepare the keys for synched use + * (see {@link getKeys}). + * @returns {Promise<importResult>} A summary and Keys considered. + * @async + * @static + */ + importKey (armored, prepare_sync) { + let feedbackValues = ['considered', 'no_user_id', 'imported', + 'imported_rsa', 'unchanged', 'new_user_ids', 'new_sub_keys', + 'new_signatures', 'new_revocations', 'secret_read', + 'secret_imported', 'secret_unchanged', 'skipped_new_keys', + 'not_imported', 'skipped_v3_keys']; + if (!armored || typeof (armored) !== 'string'){ + return Promise.reject(gpgme_error('PARAM_WRONG')); + } + let me = this; + return new Promise(function (resolve, reject){ + let msg = createMessage('import'); + msg.setParameter('data', armored); + msg.post().then(function (response){ + let infos = {}; + let fprs = []; + let summary = {}; + for (let i=0; i < feedbackValues.length; i++ ){ + summary[feedbackValues[i]] = + response.result[feedbackValues[i]]; + } + if (!response.result.hasOwnProperty('imports') || + response.result.imports.length === 0 + ){ + resolve({ Keys:[],summary: summary }); + return; + } + for (let res=0; res<response.result.imports.length; res++){ + let result = response.result.imports[res]; + let status = ''; + if (result.status === 0){ + status = 'nochange'; + } else if ((result.status & 1) === 1){ + status = 'newkey'; + } else { + status = 'change'; + } + let changes = {}; + changes.userId = (result.status & 2) === 2; + changes.signature = (result.status & 4) === 4; + changes.subkey = (result.status & 8) === 8; + // 16 new secret key: not implemented + fprs.push(result.fingerprint); + infos[result.fingerprint] = { + changes: changes, + status: status + }; + } + let resultset = []; + if (prepare_sync === true){ + me.getKeys({ pattern: fprs, prepare_sync: true }) + .then(function (result){ + for (let i=0; i < result.length; i++) { + resultset.push({ + key: result[i], + changes: + infos[result[i].fingerprint].changes, + status: infos[result[i].fingerprint].status + }); + } + resolve({ Keys:resultset,summary: summary }); + }, function (error){ + reject(error); + }); + } else { + for (let i=0; i < fprs.length; i++) { + resultset.push({ + key: createKey(fprs[i]), + changes: infos[fprs[i]].changes, + status: infos[fprs[i]].status + }); + } + resolve({ Keys:resultset,summary:summary }); + } + + }, function (error){ + reject(error); + }); + + + }); + + + } + + /** + * Convenience function for deleting a Key. See {@link Key#delete} for + * further information about the return values. + * @param {String} fingerprint + * @returns {Promise<Boolean>} + * @async + * @static + */ + deleteKey (fingerprint){ + if (isFingerprint(fingerprint) === true) { + let key = createKey(fingerprint); + return key.delete(); + } else { + return Promise.reject(gpgme_error('KEY_INVALID')); + } + } + + /** + * Generates a new Key pair directly in gpg, and returns a GPGME_Key + * representing that Key. Please note that due to security concerns, + * secret Keys can not be deleted or exported from inside gpgme.js. + * @param {Object} options + * @param {String} option.userId The user Id, e.g. 'Foo Bar <foo@bar.baz>' + * @param {String} option.algo (optional) algorithm (and optionally key + * size) to be used. See {@link supportedKeyAlgos} below for supported + * values. If ommitted, 'default' is used. + * @param {Number} option.expires (optional) Expiration time in seconds + * from now. If not set or set to 0, expiration will be 'never' + * + * @return {Promise<Key|GPGME_Error>} + * @async + */ + generateKey ({ userId, algo = 'default', expires= 0 } = {}){ + if (typeof userId !== 'string' + // eslint-disable-next-line no-use-before-define + || (algo && supportedKeyAlgos.indexOf(algo) < 0 ) + || (!Number.isInteger(expires) || expires < 0 ) + ){ + return Promise.reject(gpgme_error('PARAM_WRONG')); + } + // eslint-disable-next-line no-use-before-define + let me = this; + return new Promise(function (resolve, reject){ + let msg = createMessage('createkey'); + msg.setParameter('userid', userId); + msg.setParameter('algo', algo); + msg.setParameter('expires', expires); + msg.post().then(function (response){ + me.getKeys({ + pattern: response.fingerprint, + prepare_sync: true + }).then(function (result){ + resolve(result); + }, function (error){ + reject(error); + }); + }, function (error) { + reject(error); + }); + }); + } +} + + +/** + * List of algorithms supported for key generation. Please refer to the gnupg + * documentation for details + */ +const supportedKeyAlgos = [ + 'default', 'future-default', + 'rsa', 'rsa2048', 'rsa3072', 'rsa4096', + 'dsa', 'dsa2048', 'dsa3072', 'dsa4096', + 'elg', 'elg2048', 'elg3072', 'elg4096', + 'ed25519', + 'cv25519', + 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1', + 'NIST P-256', 'NIST P-384', 'NIST P-521' +]; diff --git a/lang/js/src/Makefile.am b/lang/js/src/Makefile.am new file mode 100644 index 0000000..dc58fd3 --- /dev/null +++ b/lang/js/src/Makefile.am @@ -0,0 +1,30 @@ +# Makefile.am for gpgme.js. +# Copyright (C) 2018 Intevation GmbH +# +# This file is part of GPGME. +# +# gpgme.js is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# gpgme.js 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA +# 02111-1307, USA + +EXTRA_DIST = Connection.js \ + Errors.js \ + gpgmejs.js \ + Helpers.js \ + index.js \ + Key.js \ + Keyring.js \ + Message.js \ + permittedOperations.js \ + Signature.js diff --git a/lang/js/src/Makefile.in b/lang/js/src/Makefile.in new file mode 100644 index 0000000..fe5e274 --- /dev/null +++ b/lang/js/src/Makefile.in @@ -0,0 +1,540 @@ +# Makefile.in generated by automake 1.14.1 from Makefile.am. +# @configure_input@ + +# Copyright (C) 1994-2013 Free Software Foundation, Inc. + +# This Makefile.in is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. + +@SET_MAKE@ + +# Makefile.am for gpgme.js. +# Copyright (C) 2018 Intevation GmbH +# +# This file is part of GPGME. +# +# gpgme.js is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# gpgme.js 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA +# 02111-1307, USA +VPATH = @srcdir@ +am__is_gnu_make = test -n '$(MAKEFILE_LIST)' && test -n '$(MAKELEVEL)' +am__make_running_with_option = \ + case $${target_option-} in \ + ?) ;; \ + *) echo "am__make_running_with_option: internal error: invalid" \ + "target option '$${target_option-}' specified" >&2; \ + exit 1;; \ + esac; \ + has_opt=no; \ + sane_makeflags=$$MAKEFLAGS; \ + if $(am__is_gnu_make); then \ + sane_makeflags=$$MFLAGS; \ + else \ + case $$MAKEFLAGS in \ + *\\[\ \ ]*) \ + bs=\\; \ + sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \ + | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \ + esac; \ + fi; \ + skip_next=no; \ + strip_trailopt () \ + { \ + flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \ + }; \ + for flg in $$sane_makeflags; do \ + test $$skip_next = yes && { skip_next=no; continue; }; \ + case $$flg in \ + *=*|--*) continue;; \ + -*I) strip_trailopt 'I'; skip_next=yes;; \ + -*I?*) strip_trailopt 'I';; \ + -*O) strip_trailopt 'O'; skip_next=yes;; \ + -*O?*) strip_trailopt 'O';; \ + -*l) strip_trailopt 'l'; skip_next=yes;; \ + -*l?*) strip_trailopt 'l';; \ + -[dEDm]) skip_next=yes;; \ + -[JT]) skip_next=yes;; \ + esac; \ + case $$flg in \ + *$$target_option*) has_opt=yes; break;; \ + esac; \ + done; \ + test $$has_opt = yes +am__make_dryrun = (target_option=n; $(am__make_running_with_option)) +am__make_keepgoing = (target_option=k; $(am__make_running_with_option)) +pkgdatadir = $(datadir)/@PACKAGE@ +pkgincludedir = $(includedir)/@PACKAGE@ +pkglibdir = $(libdir)/@PACKAGE@ +pkglibexecdir = $(libexecdir)/@PACKAGE@ +am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd +install_sh_DATA = $(install_sh) -c -m 644 +install_sh_PROGRAM = $(install_sh) -c +install_sh_SCRIPT = $(install_sh) -c +INSTALL_HEADER = $(INSTALL_DATA) +transform = $(program_transform_name) +NORMAL_INSTALL = : +PRE_INSTALL = : +POST_INSTALL = : +NORMAL_UNINSTALL = : +PRE_UNINSTALL = : +POST_UNINSTALL = : +build_triplet = @build@ +host_triplet = @host@ +subdir = lang/js/src +DIST_COMMON = $(srcdir)/Makefile.in $(srcdir)/Makefile.am \ + $(top_srcdir)/build-aux/mkinstalldirs +ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 +am__aclocal_m4_deps = $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ + $(top_srcdir)/m4/ax_pkg_swig.m4 \ + $(top_srcdir)/m4/ax_python_devel.m4 \ + $(top_srcdir)/m4/glib-2.0.m4 $(top_srcdir)/m4/glibc21.m4 \ + $(top_srcdir)/m4/gnupg-ttyname.m4 \ + $(top_srcdir)/m4/gpg-error.m4 $(top_srcdir)/m4/libassuan.m4 \ + $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/ltoptions.m4 \ + $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \ + $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/pkg.m4 \ + $(top_srcdir)/m4/python.m4 $(top_srcdir)/m4/qt.m4 \ + $(top_srcdir)/acinclude.m4 $(top_srcdir)/configure.ac +am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ + $(ACLOCAL_M4) +mkinstalldirs = $(SHELL) $(top_srcdir)/build-aux/mkinstalldirs +CONFIG_HEADER = $(top_builddir)/conf/config.h +CONFIG_CLEAN_FILES = +CONFIG_CLEAN_VPATH_FILES = +AM_V_P = $(am__v_P_@AM_V@) +am__v_P_ = $(am__v_P_@AM_DEFAULT_V@) +am__v_P_0 = false +am__v_P_1 = : +AM_V_GEN = $(am__v_GEN_@AM_V@) +am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@) +am__v_GEN_0 = @echo " GEN " $@; +am__v_GEN_1 = +AM_V_at = $(am__v_at_@AM_V@) +am__v_at_ = $(am__v_at_@AM_DEFAULT_V@) +am__v_at_0 = @ +am__v_at_1 = +SOURCES = +DIST_SOURCES = +am__can_run_installinfo = \ + case $$AM_UPDATE_INFO_DIR in \ + n|no|NO) false;; \ + *) (install-info --version) >/dev/null 2>&1;; \ + esac +am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP) +DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +ACLOCAL = @ACLOCAL@ +AMTAR = @AMTAR@ +AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@ +AR = @AR@ +AS = @AS@ +AUTOCONF = @AUTOCONF@ +AUTOHEADER = @AUTOHEADER@ +AUTOMAKE = @AUTOMAKE@ +AWK = @AWK@ +BUILD_FILEVERSION = @BUILD_FILEVERSION@ +BUILD_REVISION = @BUILD_REVISION@ +BUILD_TIMESTAMP = @BUILD_TIMESTAMP@ +CC = @CC@ +CCDEPMODE = @CCDEPMODE@ +CC_FOR_BUILD = @CC_FOR_BUILD@ +CFLAGS = @CFLAGS@ +CPP = @CPP@ +CPPFLAGS = @CPPFLAGS@ +CXX = @CXX@ +CXXCPP = @CXXCPP@ +CXXDEPMODE = @CXXDEPMODE@ +CXXFLAGS = @CXXFLAGS@ +CYGPATH_W = @CYGPATH_W@ +DEFS = @DEFS@ +DEPDIR = @DEPDIR@ +DLLTOOL = @DLLTOOL@ +DOXYGEN = @DOXYGEN@ +DSYMUTIL = @DSYMUTIL@ +DUMPBIN = @DUMPBIN@ +ECHO_C = @ECHO_C@ +ECHO_N = @ECHO_N@ +ECHO_T = @ECHO_T@ +EGREP = @EGREP@ +ENABLED_LANGUAGES = @ENABLED_LANGUAGES@ +EXEEXT = @EXEEXT@ +FGREP = @FGREP@ +GITLOG_TO_CHANGELOG = @GITLOG_TO_CHANGELOG@ +GLIBC21 = @GLIBC21@ +GLIB_CFLAGS = @GLIB_CFLAGS@ +GLIB_GENMARSHAL = @GLIB_GENMARSHAL@ +GLIB_LIBS = @GLIB_LIBS@ +GLIB_MKENUMS = @GLIB_MKENUMS@ +GOBJECT_QUERY = @GOBJECT_QUERY@ +GPGME_CONFIG_API_VERSION = @GPGME_CONFIG_API_VERSION@ +GPGME_CONFIG_AVAIL_LANG = @GPGME_CONFIG_AVAIL_LANG@ +GPGME_CONFIG_CFLAGS = @GPGME_CONFIG_CFLAGS@ +GPGME_CONFIG_HOST = @GPGME_CONFIG_HOST@ +GPGME_CONFIG_LIBS = @GPGME_CONFIG_LIBS@ +GPGME_QTTEST_CFLAGS = @GPGME_QTTEST_CFLAGS@ +GPGME_QTTEST_LIBS = @GPGME_QTTEST_LIBS@ +GPGME_QT_CFLAGS = @GPGME_QT_CFLAGS@ +GPGME_QT_LIBS = @GPGME_QT_LIBS@ +GPG_ERROR_CFLAGS = @GPG_ERROR_CFLAGS@ +GPG_ERROR_CONFIG = @GPG_ERROR_CONFIG@ +GPG_ERROR_LIBS = @GPG_ERROR_LIBS@ +GPG_ERROR_MT_CFLAGS = @GPG_ERROR_MT_CFLAGS@ +GPG_ERROR_MT_LIBS = @GPG_ERROR_MT_LIBS@ +GRAPHVIZ = @GRAPHVIZ@ +GREP = @GREP@ +HAVE_CXX11 = @HAVE_CXX11@ +HAVE_DOT = @HAVE_DOT@ +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ +INSTALL_PROGRAM = @INSTALL_PROGRAM@ +INSTALL_SCRIPT = @INSTALL_SCRIPT@ +INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@ +LD = @LD@ +LDFLAGS = @LDFLAGS@ +LIBASSUAN_CFLAGS = @LIBASSUAN_CFLAGS@ +LIBASSUAN_CONFIG = @LIBASSUAN_CONFIG@ +LIBASSUAN_LIBS = @LIBASSUAN_LIBS@ +LIBGPGMEPP_LT_AGE = @LIBGPGMEPP_LT_AGE@ +LIBGPGMEPP_LT_CURRENT = @LIBGPGMEPP_LT_CURRENT@ +LIBGPGMEPP_LT_REVISION = @LIBGPGMEPP_LT_REVISION@ +LIBGPGME_LT_AGE = @LIBGPGME_LT_AGE@ +LIBGPGME_LT_CURRENT = @LIBGPGME_LT_CURRENT@ +LIBGPGME_LT_REVISION = @LIBGPGME_LT_REVISION@ +LIBOBJS = @LIBOBJS@ +LIBQGPGME_LT_AGE = @LIBQGPGME_LT_AGE@ +LIBQGPGME_LT_CURRENT = @LIBQGPGME_LT_CURRENT@ +LIBQGPGME_LT_REVISION = @LIBQGPGME_LT_REVISION@ +LIBS = @LIBS@ +LIBTOOL = @LIBTOOL@ +LIPO = @LIPO@ +LN_S = @LN_S@ +LTLIBOBJS = @LTLIBOBJS@ +MAINT = @MAINT@ +MAKEINFO = @MAKEINFO@ +MANIFEST_TOOL = @MANIFEST_TOOL@ +MKDIR_P = @MKDIR_P@ +MOC = @MOC@ +MOC2 = @MOC2@ +NEED__FILE_OFFSET_BITS = @NEED__FILE_OFFSET_BITS@ +NM = @NM@ +NMEDIT = @NMEDIT@ +OBJDUMP = @OBJDUMP@ +OBJEXT = @OBJEXT@ +OTOOL = @OTOOL@ +OTOOL64 = @OTOOL64@ +PACKAGE = @PACKAGE@ +PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@ +PACKAGE_NAME = @PACKAGE_NAME@ +PACKAGE_STRING = @PACKAGE_STRING@ +PACKAGE_TARNAME = @PACKAGE_TARNAME@ +PACKAGE_URL = @PACKAGE_URL@ +PACKAGE_VERSION = @PACKAGE_VERSION@ +PATH_SEPARATOR = @PATH_SEPARATOR@ +PKG_CONFIG = @PKG_CONFIG@ +PYTHON = @PYTHON@ +PYTHONS = @PYTHONS@ +PYTHON_CPPFLAGS = @PYTHON_CPPFLAGS@ +PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@ +PYTHON_EXTRA_LDFLAGS = @PYTHON_EXTRA_LDFLAGS@ +PYTHON_EXTRA_LIBS = @PYTHON_EXTRA_LIBS@ +PYTHON_LDFLAGS = @PYTHON_LDFLAGS@ +PYTHON_PLATFORM = @PYTHON_PLATFORM@ +PYTHON_PREFIX = @PYTHON_PREFIX@ +PYTHON_SITE_PKG = @PYTHON_SITE_PKG@ +PYTHON_VERSION = @PYTHON_VERSION@ +QTCHOOSER = @QTCHOOSER@ +RANLIB = @RANLIB@ +RC = @RC@ +SED = @SED@ +SET_MAKE = @SET_MAKE@ +SHELL = @SHELL@ +STRIP = @STRIP@ +SWIG = @SWIG@ +SWIG_LIB = @SWIG_LIB@ +SYSROOT = @SYSROOT@ +VERSION = @VERSION@ +VERSION_MAJOR = @VERSION_MAJOR@ +VERSION_MICRO = @VERSION_MICRO@ +VERSION_MINOR = @VERSION_MINOR@ +VERSION_NUMBER = @VERSION_NUMBER@ +abs_builddir = @abs_builddir@ +abs_srcdir = @abs_srcdir@ +abs_top_builddir = @abs_top_builddir@ +abs_top_srcdir = @abs_top_srcdir@ +ac_ct_AR = @ac_ct_AR@ +ac_ct_CC = @ac_ct_CC@ +ac_ct_CXX = @ac_ct_CXX@ +ac_ct_DUMPBIN = @ac_ct_DUMPBIN@ +am__include = @am__include@ +am__leading_dot = @am__leading_dot@ +am__quote = @am__quote@ +am__tar = @am__tar@ +am__untar = @am__untar@ +bindir = @bindir@ +build = @build@ +build_alias = @build_alias@ +build_cpu = @build_cpu@ +build_os = @build_os@ +build_vendor = @build_vendor@ +builddir = @builddir@ +datadir = @datadir@ +datarootdir = @datarootdir@ +docdir = @docdir@ +dvidir = @dvidir@ +emacs_local_vars_begin = @emacs_local_vars_begin@ +emacs_local_vars_end = @emacs_local_vars_end@ +emacs_local_vars_read_only = @emacs_local_vars_read_only@ +exec_prefix = @exec_prefix@ +host = @host@ +host_alias = @host_alias@ +host_cpu = @host_cpu@ +host_os = @host_os@ +host_vendor = @host_vendor@ +htmldir = @htmldir@ +includedir = @includedir@ +infodir = @infodir@ +install_sh = @install_sh@ +libdir = @libdir@ +libexecdir = @libexecdir@ +localedir = @localedir@ +localstatedir = @localstatedir@ +mandir = @mandir@ +mkdir_p = @mkdir_p@ +oldincludedir = @oldincludedir@ +pdfdir = @pdfdir@ +pkgpyexecdir = @pkgpyexecdir@ +pkgpythondir = @pkgpythondir@ +prefix = @prefix@ +program_transform_name = @program_transform_name@ +psdir = @psdir@ +pyexecdir = @pyexecdir@ +pythondir = @pythondir@ +sbindir = @sbindir@ +sharedstatedir = @sharedstatedir@ +srcdir = @srcdir@ +sysconfdir = @sysconfdir@ +target_alias = @target_alias@ +top_build_prefix = @top_build_prefix@ +top_builddir = @top_builddir@ +top_srcdir = @top_srcdir@ +EXTRA_DIST = Connection.js \ + Errors.js \ + gpgmejs.js \ + Helpers.js \ + index.js \ + Key.js \ + Keyring.js \ + Message.js \ + permittedOperations.js \ + Signature.js + +all: all-am + +.SUFFIXES: +$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps) + @for dep in $?; do \ + case '$(am__configure_deps)' in \ + *$$dep*) \ + ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \ + && { if test -f $@; then exit 0; else break; fi; }; \ + exit 1;; \ + esac; \ + done; \ + echo ' cd $(top_srcdir) && $(AUTOMAKE) --gnu lang/js/src/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --gnu lang/js/src/Makefile +.PRECIOUS: Makefile +Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status + @case '$?' in \ + *config.status*) \ + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ + *) \ + echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe)'; \ + cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe);; \ + esac; + +$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh + +$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(am__aclocal_m4_deps): + +mostlyclean-libtool: + -rm -f *.lo + +clean-libtool: + -rm -rf .libs _libs +tags TAGS: + +ctags CTAGS: + +cscope cscopelist: + + +distdir: $(DISTFILES) + @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + list='$(DISTFILES)'; \ + dist_files=`for file in $$list; do echo $$file; done | \ + sed -e "s|^$$srcdirstrip/||;t" \ + -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ + case $$dist_files in \ + */*) $(MKDIR_P) `echo "$$dist_files" | \ + sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ + sort -u` ;; \ + esac; \ + for file in $$dist_files; do \ + if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ + if test -d $$d/$$file; then \ + dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ + if test -d "$(distdir)/$$file"; then \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ + cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ + else \ + test -f "$(distdir)/$$file" \ + || cp -p $$d/$$file "$(distdir)/$$file" \ + || exit 1; \ + fi; \ + done +check-am: all-am +check: check-am +all-am: Makefile +installdirs: +install: install-am +install-exec: install-exec-am +install-data: install-data-am +uninstall: uninstall-am + +install-am: all-am + @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am + +installcheck: installcheck-am +install-strip: + if test -z '$(STRIP)'; then \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + install; \ + else \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \ + fi +mostlyclean-generic: + +clean-generic: + +distclean-generic: + -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES) + -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES) + +maintainer-clean-generic: + @echo "This command is intended for maintainers to use" + @echo "it deletes files that may require special tools to rebuild." +clean: clean-am + +clean-am: clean-generic clean-libtool mostlyclean-am + +distclean: distclean-am + -rm -f Makefile +distclean-am: clean-am distclean-generic + +dvi: dvi-am + +dvi-am: + +html: html-am + +html-am: + +info: info-am + +info-am: + +install-data-am: + +install-dvi: install-dvi-am + +install-dvi-am: + +install-exec-am: + +install-html: install-html-am + +install-html-am: + +install-info: install-info-am + +install-info-am: + +install-man: + +install-pdf: install-pdf-am + +install-pdf-am: + +install-ps: install-ps-am + +install-ps-am: + +installcheck-am: + +maintainer-clean: maintainer-clean-am + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic + +mostlyclean: mostlyclean-am + +mostlyclean-am: mostlyclean-generic mostlyclean-libtool + +pdf: pdf-am + +pdf-am: + +ps: ps-am + +ps-am: + +uninstall-am: + +.MAKE: install-am install-strip + +.PHONY: all all-am check check-am clean clean-generic clean-libtool \ + cscopelist-am ctags-am distclean distclean-generic \ + distclean-libtool distdir dvi dvi-am html html-am info info-am \ + install install-am install-data install-data-am install-dvi \ + install-dvi-am install-exec install-exec-am install-html \ + install-html-am install-info install-info-am install-man \ + install-pdf install-pdf-am install-ps install-ps-am \ + install-strip installcheck installcheck-am installdirs \ + maintainer-clean maintainer-clean-generic mostlyclean \ + mostlyclean-generic mostlyclean-libtool pdf pdf-am ps ps-am \ + tags-am uninstall uninstall-am + + +# Tell versions [3.59,3.63) of GNU make to not export all variables. +# Otherwise a system limit (for SysV at least) may be exceeded. +.NOEXPORT: diff --git a/lang/js/src/Message.js b/lang/js/src/Message.js new file mode 100644 index 0000000..9f6abb7 --- /dev/null +++ b/lang/js/src/Message.js @@ -0,0 +1,243 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +import { permittedOperations } from './permittedOperations'; +import { gpgme_error } from './Errors'; +import { Connection } from './Connection'; + +/** + * Initializes a message for gnupg, validating the message's purpose with + * {@link permittedOperations} first + * @param {String} operation + * @returns {GPGME_Message} The Message object + */ +export function createMessage (operation){ + if (typeof (operation) !== 'string'){ + throw gpgme_error('PARAM_WRONG'); + } + if (permittedOperations.hasOwnProperty(operation)){ + return new GPGME_Message(operation); + } else { + throw gpgme_error('MSG_WRONG_OP'); + } +} + +/** + * A Message collects, validates and handles all information required to + * successfully establish a meaningful communication with gpgme-json via + * [Connection.post]{@link Connection#post}. The definition on which + * communication is available can be found in {@link permittedOperations}. + * @class + * @protected + */ +export class GPGME_Message { + + constructor (operation){ + this._msg = { + op: operation, + chunksize: 1023* 1024 + }; + this._expected = null; + } + + get operation (){ + return this._msg.op; + } + + set expected (value){ + if (value === 'uint8' || value === 'base64'){ + this._expected = value; + } + } + + get expected () { + return this._expected; + } + /** + * The maximum size of responses from gpgme in bytes. As of September 2018, + * most browsers will only accept answers up to 1 MB of size. + * Everything above that threshold will not pass through + * nativeMessaging; answers that are larger need to be sent in parts. + * The lower limit is set to 10 KB. Messages smaller than the threshold + * will not encounter problems, larger messages will be received in + * chunks. If the value is not explicitly specified, 1023 KB is used. + */ + set chunksize (value){ + if ( + Number.isInteger(value) && + value > 10 * 1024 && + value <= 1024 * 1024 + ){ + this._msg.chunksize = value; + } + } + + get chunksize (){ + return this._msg.chunksize; + } + + /** + * Returns the prepared message after their parameters and the completion + * of required parameters have been checked. + * @returns {Object|null} Object to be posted to gnupg, or null if + * incomplete + */ + get message () { + if (this.isComplete() === true){ + return this._msg; + } else { + return null; + } + } + + /** + * Sets a parameter for the message. It validates with + * {@link permittedOperations} + * @param {String} param Parameter to set + * @param {any} value Value to set + * @returns {Boolean} True if the parameter was set successfully. + * Throws errors if the parameters don't match the message operation + */ + setParameter ( param,value ){ + if (!param || typeof (param) !== 'string'){ + throw gpgme_error('PARAM_WRONG'); + } + let po = permittedOperations[this._msg.op]; + if (!po){ + throw gpgme_error('MSG_WRONG_OP'); + } + let poparam = null; + if (po.required.hasOwnProperty(param)){ + poparam = po.required[param]; + } else if (po.optional.hasOwnProperty(param)){ + poparam = po.optional[param]; + } else { + throw gpgme_error('PARAM_WRONG'); + } + // check incoming value for correctness + let checktype = function (val){ + switch (typeof (val)){ + case 'string': + if (poparam.allowed.indexOf(typeof (val)) >= 0 + && val.length > 0) { + return true; + } + throw gpgme_error('PARAM_WRONG'); + case 'number': + if ( + poparam.allowed.indexOf('number') >= 0 + && isNaN(value) === false){ + return true; + } + throw gpgme_error('PARAM_WRONG'); + + case 'boolean': + if (poparam.allowed.indexOf('boolean') >= 0){ + return true; + } + throw gpgme_error('PARAM_WRONG'); + case 'object': + if (Array.isArray(val)){ + if (poparam.array_allowed !== true){ + throw gpgme_error('PARAM_WRONG'); + } + for (let i=0; i < val.length; i++){ + let res = checktype(val[i]); + if (res !== true){ + return res; + } + } + if (val.length > 0) { + return true; + } + } else if (val instanceof Uint8Array){ + if (poparam.allowed.indexOf('Uint8Array') >= 0){ + return true; + } + throw gpgme_error('PARAM_WRONG'); + } else { + throw gpgme_error('PARAM_WRONG'); + } + break; + default: + throw gpgme_error('PARAM_WRONG'); + } + }; + let typechecked = checktype(value); + if (typechecked !== true){ + return typechecked; + } + if (poparam.hasOwnProperty('allowed_data')){ + if (poparam.allowed_data.indexOf(value) < 0){ + return gpgme_error('PARAM_WRONG'); + } + } + this._msg[param] = value; + return true; + } + + + /** + * Check if the message has the minimum requirements to be sent, that is + * all 'required' parameters according to {@link permittedOperations}. + * @returns {Boolean} true if message is complete. + */ + isComplete (){ + if (!this._msg.op){ + return false; + } + let reqParams = Object.keys( + permittedOperations[this._msg.op].required); + let msg_params = Object.keys(this._msg); + for (let i=0; i < reqParams.length; i++){ + if (msg_params.indexOf(reqParams[i]) < 0){ + return false; + } + } + return true; + } + + /** + * Sends the Message via nativeMessaging and resolves with the answer. + * @returns {Promise<Object>} GPGME response + * @async + */ + post (){ + let me = this; + return new Promise(function (resolve, reject) { + if (me.isComplete() === true) { + + let conn = new Connection; + conn.post(me).then(function (response) { + resolve(response); + }, function (reason) { + reject(reason); + }); + } + else { + reject(gpgme_error('MSG_INCOMPLETE')); + } + }); + } + +} diff --git a/lang/js/src/Signature.js b/lang/js/src/Signature.js new file mode 100644 index 0000000..f848e32 --- /dev/null +++ b/lang/js/src/Signature.js @@ -0,0 +1,206 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ +import { gpgme_error } from './Errors'; + +/** + * Validates an object containing a signature, as sent by the nativeMessaging + * interface + * @param {Object} sigObject Object as returned by gpgme-json. The definition + * of the expected values are to be found in {@link expKeys}, {@link expSum}, + * {@link expNote}. + * @returns {GPGME_Signature|GPGME_Error} Signature Object + * @private + */ +export function createSignature (sigObject){ + if ( + typeof (sigObject) !=='object' || + !sigObject.hasOwnProperty('summary') || + !sigObject.hasOwnProperty('fingerprint') || + !sigObject.hasOwnProperty('timestamp') + // TODO check if timestamp is mandatory in specification + ){ + return gpgme_error('SIG_WRONG'); + } + let keys = Object.keys(sigObject); + for (let i=0; i< keys.length; i++){ + // eslint-disable-next-line no-use-before-define + if ( typeof (sigObject[keys[i]]) !== expKeys[keys[i]] ){ + return gpgme_error('SIG_WRONG'); + } + } + let sumkeys = Object.keys(sigObject.summary); + for (let i=0; i< sumkeys.length; i++){ + // eslint-disable-next-line no-use-before-define + if ( typeof (sigObject.summary[sumkeys[i]]) !== expSum[sumkeys[i]] ){ + return gpgme_error('SIG_WRONG'); + } + } + if (sigObject.hasOwnProperty('notations')){ + if (!Array.isArray(sigObject.notations)){ + return gpgme_error('SIG_WRONG'); + } + for (let i=0; i < sigObject.notations.length; i++){ + let notation = sigObject.notations[i]; + let notekeys = Object.keys(notation); + for (let j=0; j < notekeys.length; j++){ + // eslint-disable-next-line no-use-before-define + if ( typeof (notation[notekeys[j]]) !== expNote[notekeys[j]] ){ + return gpgme_error('SIG_WRONG'); + } + } + } + } + return new GPGME_Signature(sigObject); +} + + +/** + * Representing the details of a signature. The full details as given by + * gpgme-json can be read from the _rawSigObject. + * + * Note to reviewers: This class should be read only except via + * {@link createSignature} + * @protected + * @class + */ +class GPGME_Signature { + + constructor (sigObject){ + this._rawSigObject = sigObject; + } + /** + * @returns {String} the fingerprint of this signature + */ + get fingerprint (){ + if (!this._rawSigObject.fingerprint){ + throw gpgme_error('SIG_WRONG'); + } else { + return this._rawSigObject.fingerprint; + } + } + + /** + * The expiration of this Signature as Javascript date, or null if + * signature does not expire + * @returns {Date | null} + */ + get expiration (){ + if (!this._rawSigObject.exp_timestamp){ + return null; + } + return new Date(this._rawSigObject.exp_timestamp* 1000); + } + + /** + * The creation date of this Signature in Javascript Date + * @returns {Date} + */ + get timestamp (){ + return new Date(this._rawSigObject.timestamp * 1000); + } + + /** + * The overall validity of the key. If false, errorDetails may contain + * additional information. + */ + get valid () { + if (this._rawSigObject.summary.valid === true){ + return true; + } else { + return false; + } + } + + /** + * Object with boolean properties giving more information on non-valid + * signatures. Refer to the [gpgme docs]{@link https://www.gnupg.org/documentation/manuals/gpgme/Verify.html} + * for details on the values. + */ + get errorDetails (){ + let properties = ['revoked', 'key-expired', 'sig-expired', + 'key-missing', 'crl-missing', 'crl-too-old', 'bad-policy', + 'sys-error']; + let result = {}; + for (let i=0; i< properties.length; i++){ + if ( this._rawSigObject.summary.hasOwnProperty(properties[i]) ){ + result[properties[i]] = this._rawSigObject.summary[properties[i]]; + } + } + return result; + } +} + +/** + * Expected keys and their value's type for the signature Object + * @private + */ +const expKeys = { + 'wrong_key_usage': 'boolean', + 'chain_model': 'boolean', + 'summary': 'object', + 'is_de_vs': 'boolean', + 'status_string':'string', + 'fingerprint':'string', + 'validity_string': 'string', + 'pubkey_algo_name':'string', + 'hash_algo_name':'string', + 'pka_address':'string', + 'status_code':'number', + 'timestamp':'number', + 'exp_timestamp':'number', + 'pka_trust':'number', + 'validity':'number', + 'validity_reason':'number', + 'notations': 'object' +}; + +/** + * Keys and their value's type for the summary + * @private + */ +const expSum = { + 'valid': 'boolean', + 'green': 'boolean', + 'red': 'boolean', + 'revoked': 'boolean', + 'key-expired': 'boolean', + 'sig-expired': 'boolean', + 'key-missing': 'boolean', + 'crl-missing': 'boolean', + 'crl-too-old': 'boolean', + 'bad-policy': 'boolean', + 'sys-error': 'boolean', + 'sigsum': 'object' +}; + +/** + * Keys and their value's type for notations objects + * @private + */ +const expNote = { + 'human_readable': 'boolean', + 'critical':'boolean', + 'name': 'string', + 'value': 'string', + 'flags': 'number' +}; diff --git a/lang/js/src/gpgmejs.js b/lang/js/src/gpgmejs.js new file mode 100644 index 0000000..9105724 --- /dev/null +++ b/lang/js/src/gpgmejs.js @@ -0,0 +1,465 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + + +import { GPGME_Message, createMessage } from './Message'; +import { toKeyIdArray } from './Helpers'; +import { gpgme_error } from './Errors'; +import { GPGME_Keyring } from './Keyring'; +import { createSignature } from './Signature'; + +/** + * @typedef {Object} decrypt_result + * @property {String|Uint8Array} data The decrypted data. + * @property {String} format Indicating how the data was converted after being + * received from gpgme: + * <pre> + * 'ascii': Data was ascii-encoded and no further processed + * 'string': Data was decoded into an utf-8 string, + * 'base64': Data was not processed and is a base64 string + * 'uint8': data was turned into a Uint8Array + * </pre> + * @property {Boolean} is_mime (optional) the data claims to be a MIME object. + * @property {String} file_name (optional) the original file name + * @property {signatureDetails} signatures Verification details for + * signatures + */ + +/** + * @typedef {Object} signatureDetails + * @property {Boolean} all_valid Quick summary. True if all signatures are + * fully valid according to gnupg. + * @property {Number} count Number of signatures parsed. + * @property {Number} failures Number of signatures not passing as valid. This + * may imply bad signatures, or signatures with e.g. the public Key not being + * available. + * @property {GPGME_Signature[]} signatures.good Array of all signatures + * considered valid. + * @property {GPGME_Signature[]} signatures.bad All invalid signatures. + */ + +/** + * @typedef {Object} encrypt_result The result of an encrypt operation, + * containing the encrypted data and some additional information. + * @property {String} data The encrypted message. + * @property {String} format Indicating how the data was converted after being + * received from gpgme. + * <pre> + * 'ascii': Data was ascii-encoded and no further processed + * 'string': Data was decoded into an utf-8 string, + * 'base64': Data was not processed and is a base64 string + * 'uint8': Data was turned into a Uint8Array + * </pre> + */ + +/** + * @typedef { GPGME_Key | String | Object } inputKeys + * Accepts different identifiers of a gnupg Key that can be parsed by + * {@link toKeyIdArray}. Expected inputs are: One or an array of + * GPGME_Keys; one or an array of fingerprint strings; one or an array of + * openpgpjs Key objects. + */ + +/** + * @typedef {Object} signResult The result of a signing operation + * @property {String} data The resulting data. Includes the signature in + * clearsign mode + * @property {String} signature The detached signature (only present in in + * detached mode) + */ + +/** @typedef {Object} verifyResult The result of a verification + * @property {Boolean} data: The verified data + * @property {Boolean} is_mime (optional) the data claims to be a MIME + * object. + * @property {signatureDetails} signatures Verification details for + * signatures + */ + +/** + * The main entry point for gpgme.js. + * @class + */ +export class GpgME { + + constructor (){ + this._Keyring = null; + } + + set Keyring (keyring){ + if (keyring && keyring instanceof GPGME_Keyring){ + this._Keyring = keyring; + } + } + + /** + * Accesses the {@link GPGME_Keyring}. From the Keyring, all Keys can be + * accessed. + */ + get Keyring (){ + if (!this._Keyring){ + this._Keyring = new GPGME_Keyring; + } + return this._Keyring; + } + + /** + * Encrypt data for the recipients specified in publicKeys. If privateKeys + * are submitted, the data will be signed by those Keys. + * @param {Object} options + * @param {String|Object} options.data text/data to be encrypted as String. + * Also accepts Objects with a getText method. + * @param {inputKeys} options.publicKeys + * Keys used to encrypt the message + * @param {inputKeys} options.secretKeys (optional) Keys used to sign the + * message. If Keys are present, the operation requested is assumed + * to be 'encrypt and sign' + * @param {Boolean} options.base64 (optional, default: false) The data will + * be interpreted as base64 encoded data. + * @param {Boolean} options.armor (optional, default: true) Request the + * output as armored block. + * @param {Boolean} options.wildcard (optional, default: false) If true, + * recipient information will not be added to the message. + * @param {Boolean} options.always_trust (optional, default true) This + * assumes that used keys are fully trusted. If set to false, encryption to + * a key not fully trusted in gnupg will fail. + * @param {String} options.expect (default: 'base64') In case of + * armored:false, request how to return the binary result. + * Accepts 'base64' or 'uint8' + * @param {Object} options.additional use additional valid gpg options as + * defined in {@link permittedOperations} + * @returns {Promise<encrypt_result>} Object containing the encrypted + * message and additional info. + * @async + */ + encrypt ({ data, publicKeys, secretKeys, base64 = false, armor = true, + wildcard, always_trust = true, expect = 'base64', + additional = {} } = {}){ + if (typeof arguments[0] !== 'object') { + return Promise.reject(gpgme_error('PARAM_WRONG')); + } + if (!data || !publicKeys){ + return Promise.reject(gpgme_error('MSG_INCOMPLETE')); + } + let msg = createMessage('encrypt'); + if (msg instanceof Error){ + return Promise.reject(msg); + } + if (armor === false){ + msg.setParameter('armor', false); + if (expect === 'uint8' || expect === 'base64') { + msg.expected = expect; + } else { + return Promise.reject(gpgme_error('PARAM_WRONG')); + } + } else if (armor === true) { + msg.setParameter('armor', true); + } + if (base64 === true) { + msg.setParameter('base64', true); + } + if (always_trust === true) { + msg.setParameter('always-trust', true); + } + let pubkeys = toKeyIdArray(publicKeys); + if (!pubkeys.length) { + return Promise.reject(gpgme_error('MSG_NO_KEYS')); + } + msg.setParameter('keys', pubkeys); + let sigkeys = toKeyIdArray(secretKeys); + if (sigkeys.length > 0) { + msg.setParameter('signing_keys', sigkeys); + } + putData(msg, data); + if (wildcard === true){ + msg.setParameter('throw-keyids', true); + } + if (additional){ + let additional_Keys = Object.keys(additional); + for (let k = 0; k < additional_Keys.length; k++) { + try { + msg.setParameter(additional_Keys[k], + additional[additional_Keys[k]]); + } + catch (error){ + return Promise.reject(error); + } + } + } + if (msg.isComplete() === true){ + return msg.post(); + } else { + return Promise.reject(gpgme_error('MSG_INCOMPLETE')); + } + } + + /** + * Decrypts (and verifies, if applicable) a message. + * @param {Object} options + * @param {String|Object} options.data text/data to be decrypted. Accepts + * Strings and Objects with a getText method. + * @param {Boolean} options.base64 (optional, default: false). Indicate that + * the input given is base64-encoded binary instead of an armored block in + * gpg armored form. + * @param {String} options.expect (optional). By default, the output is + * expected to be a string compatible with javascript. In cases of binary + * data the decryption may fail due to encoding problems. For data expected + * to return as binary data, the decroding after decryption can be bypassed: + * <pre> + * 'uint8': Return as Uint8Array + * 'base64': Return as unprocessed (base64 encoded) string. + * </pre> + * @returns {Promise<decrypt_result>} Decrypted Message and information + * @async + */ + decrypt ({ data, base64, expect } = {}){ + if (typeof arguments[0] !== 'object') { + return Promise.reject(gpgme_error('PARAM_WRONG')); + } + if (!data){ + return Promise.reject(gpgme_error('MSG_EMPTY')); + } + let msg = createMessage('decrypt'); + + if (msg instanceof Error){ + return Promise.reject(msg); + } + if (base64 === true){ + msg.setParameter('base64', true); + } + if (expect === 'base64' || expect === 'uint8'){ + msg.expected = expect; + } + putData(msg, data); + return new Promise(function (resolve, reject){ + msg.post().then(function (result){ + let returnValue = { data: result.data }; + returnValue.format = result.format ? result.format : null; + if (result.hasOwnProperty('dec_info')){ + returnValue.is_mime = result.dec_info.is_mime ? true: false; + if (result.dec_info.file_name) { + returnValue.file_name = result.dec_info.file_name; + } + } + if (!returnValue.file_name) { + returnValue.file_name = null; + } + if (result.hasOwnProperty('info') + && result.info.hasOwnProperty('signatures') + && Array.isArray(result.info.signatures) + ) { + returnValue.signatures = collectSignatures( + result.info.signatures); + } + if (returnValue.signatures instanceof Error){ + reject(returnValue.signatures); + } else { + resolve(returnValue); + } + }, function (error){ + reject(error); + }); + }); + } + + /** + * Sign a Message. + * @param {Object} options Signing options + * @param {String|Object} options.data text/data to be signed. Accepts + * Strings and Objects with a getText method. + * @param {inputKeys} options.keys The key/keys to use for signing + * @param {String} options.mode The signing mode. Currently supported: + * <pre> + * 'clearsign':The Message is embedded into the signature; + * 'detached': The signature is stored separately + * </pre> + * @param {Boolean} options.base64 input is considered base64 + * @returns {Promise<signResult>} + * @async + */ + sign ({ data, keys, mode = 'clearsign', base64 } = {}){ + if (typeof arguments[0] !== 'object') { + return Promise.reject(gpgme_error('PARAM_WRONG')); + } + if (!data){ + return Promise.reject(gpgme_error('MSG_EMPTY')); + } + let key_arr = toKeyIdArray(keys); + if (key_arr.length === 0){ + return Promise.reject(gpgme_error('MSG_NO_KEYS')); + } + + let msg = createMessage('sign'); + msg.setParameter('keys', key_arr); + if (base64 === true){ + msg.setParameter('base64', true); + } + msg.setParameter('mode', mode); + putData(msg, data); + + return new Promise(function (resolve,reject) { + msg.post().then( function (message) { + if (mode === 'clearsign'){ + resolve({ + data: message.data } + ); + } else if (mode === 'detached') { + resolve({ + data: data, + signature: message.data + }); + } + }, function (error){ + reject(error); + }); + }); + } + + /** + * Verifies data. + * @param {Object} options + * @param {String|Object} options.data text/data to be verified. Accepts + * Strings and Objects with a getText method + * @param {String} options.signature A detached signature. If not present, + * opaque mode is assumed + * @param {Boolean} options.base64 Indicating that data and signature are + * base64 encoded + * @returns {Promise<verifyResult>} + *@async + */ + verify ({ data, signature, base64 } = {}){ + if (typeof arguments[0] !== 'object') { + return Promise.reject(gpgme_error('PARAM_WRONG')); + } + if (!data){ + return Promise.reject(gpgme_error('PARAM_WRONG')); + } + let msg = createMessage('verify'); + let dt = putData(msg, data); + if (dt instanceof Error){ + return Promise.reject(dt); + } + if (signature){ + if (typeof signature !== 'string'){ + return Promise.reject(gpgme_error('PARAM_WRONG')); + } else { + msg.setParameter('signature', signature); + } + } + if (base64 === true){ + msg.setParameter('base64', true); + } + return new Promise(function (resolve, reject){ + msg.post().then(function (message){ + if (!message.info || !message.info.signatures){ + reject(gpgme_error('SIG_NO_SIGS')); + } else { + let returnValue = { + signatures: collectSignatures(message.info.signatures) + }; + if (returnValue.signatures instanceof Error){ + reject(returnValue.signatures); + } else { + returnValue.is_mime = message.info.is_mime? true: false; + if (message.info.filename){ + returnValue.file_name = message.info.filename; + } + returnValue.data = message.data; + resolve(returnValue); + } + } + }, function (error){ + reject(error); + }); + }); + } +} + +/** + * Sets the data of the message, setting flags according on the data type + * @param {GPGME_Message} message The message where this data will be set + * @param { String| Object } data The data to enter. Expects either a string of + * data, or an object with a getText method + * @returns {undefined| GPGME_Error} Error if not successful, nothing otherwise + * @private + */ +function putData (message, data){ + if (!message || !(message instanceof GPGME_Message)) { + return gpgme_error('PARAM_WRONG'); + } + if (!data){ + return gpgme_error('PARAM_WRONG'); + } else if (typeof data === 'string') { + message.setParameter('data', data); + } else if ( + (typeof data === 'object') && + (typeof data.getText === 'function') + ){ + let txt = data.getText(); + if (typeof txt === 'string'){ + message.setParameter('data', txt); + } else { + return gpgme_error('PARAM_WRONG'); + } + + } else { + return gpgme_error('PARAM_WRONG'); + } +} + +/** + * Parses, validates and converts incoming objects into signatures. + * @param {Array<Object>} sigs + * @returns {signatureDetails} Details about the signatures + * @private + */ +function collectSignatures (sigs){ + if (!Array.isArray(sigs)){ + return gpgme_error('SIG_NO_SIGS'); + } + let summary = { + all_valid: false, + count: sigs.length, + failures: 0, + signatures: { + good: [], + bad: [], + } + }; + for (let i=0; i< sigs.length; i++){ + let sigObj = createSignature(sigs[i]); + if (sigObj instanceof Error) { + return gpgme_error('SIG_WRONG'); + } + if (sigObj.valid !== true){ + summary.failures += 1; + summary.signatures.bad.push(sigObj); + } else { + summary.signatures.good.push(sigObj); + } + } + if (summary.failures === 0){ + summary.all_valid = true; + } + return summary; +}
\ No newline at end of file diff --git a/lang/js/src/index.js b/lang/js/src/index.js new file mode 100644 index 0000000..b8e4274 --- /dev/null +++ b/lang/js/src/index.js @@ -0,0 +1,58 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + + +import { GpgME } from './gpgmejs'; +import { gpgme_error } from './Errors'; +import { Connection } from './Connection'; + +/** + * Main entry point for gpgme.js. It initializes by testing the nativeMessaging + * connection once, and then offers the available functions as method of the + * response object. + * An unsuccessful attempt will reject as a GPGME_Error. + * @param {Object} config (optional) configuration options + * @param {Number} config.timeout set the timeout for the initial connection + * check. On some machines and operating systems a default timeout of 500 ms is + * too low, so a higher number might be attempted. + * @returns {Promise<GpgME>} + * @async + */ +function init ({ timeout = 500 } = {}){ + return new Promise(function (resolve, reject){ + const connection = new Connection; + connection.checkConnection(false, timeout).then( + function (result){ + if (result === true) { + resolve(new GpgME()); + } else { + reject(gpgme_error('CONN_NO_CONNECT')); + } + }, function (){ // unspecific connection error. Should not happen + reject(gpgme_error('CONN_NO_CONNECT')); + }); + }); +} + +const exportvalue = { init:init }; +export default exportvalue;
\ No newline at end of file diff --git a/lang/js/src/permittedOperations.js b/lang/js/src/permittedOperations.js new file mode 100644 index 0000000..09a1783 --- /dev/null +++ b/lang/js/src/permittedOperations.js @@ -0,0 +1,413 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * Author(s): + * Maximilian Krambach <mkrambach@intevation.de> + */ + +/** + * @typedef {Object} messageProperty + * A message Property is defined by it's key. + * @property {Array<String>} allowed Array of allowed types. + * Currently accepted values are 'number', 'string', 'boolean'. + * @property {Boolean} array_allowed If the value can be an array of types + * defined in allowed + * @property {Array<*>} allowed_data (optional) restricts to the given values + */ + +/** + * Definition of the possible interactions with gpgme-json. + * @param {Object} operation Each operation is named by a key and contains + * the following properties: + * @property {messageProperty} required An object with all required parameters + * @property {messageProperty} optional An object with all optional parameters + * @property {Boolean} pinentry (optional) If true, a password dialog is + * expected, thus a connection tuimeout is not advisable + * @property {Object} answer The definition on what to expect as answer, if the + * answer is not an error + * @property {Array<String>} answer.type the type(s) as reported by gpgme-json. + * @property {Object} answer.payload key-value combinations of expected + * properties of an answer and their type ('boolean', 'string', object), which + * may need further decoding from base64 + * @property {Object} answer.info key-value combinations of expected + * properties of an answer and their type ('boolean', 'string', object), which + * are meant to be data directly sent by gpgme (i.e. user ids) + @const +*/ +export const permittedOperations = { + encrypt: { + pinentry: true, // TODO only with signing_keys + required: { + 'keys': { + allowed: ['string'], + array_allowed: true + }, + 'data': { + allowed: ['string'] + } + }, + optional: { + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + 'signing_keys': { + allowed: ['string'], + array_allowed: true + }, + 'base64': { + allowed: ['boolean'] + }, + 'mime': { + allowed: ['boolean'] + }, + 'armor': { + allowed: ['boolean'] + }, + 'always-trust': { + allowed: ['boolean'] + }, + 'no-encrypt-to': { + allowed: ['string'], + array_allowed: true + }, + 'no-compress': { + allowed: ['boolean'] + }, + 'throw-keyids': { + allowed: ['boolean'] + }, + 'want-address': { + allowed: ['boolean'] + }, + 'wrap': { + allowed: ['boolean'] + }, + 'sender': { + allowed: ['string'] + }, + 'file_name': { + allowed: ['string'] + } + }, + answer: { + type: ['ciphertext'], + payload: { + 'data': 'string' + }, + info: { + 'base64':'boolean' + } + } + }, + + decrypt: { + pinentry: true, + required: { + 'data': { + allowed: ['string'] + } + }, + optional: { + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + 'base64': { + allowed: ['boolean'] + } + }, + answer: { + type: ['plaintext'], + payload: { + 'data': 'string', + }, + info: { + 'base64': 'boolean', + 'mime': 'boolean', + 'info': 'object', + 'dec_info': 'object' + } + } + }, + + sign: { + pinentry: true, + required: { + 'data': { + allowed: ['string'] }, + 'keys': { + allowed: ['string'], + array_allowed: true + } + }, + optional: { + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + 'sender': { + allowed: ['string'], + }, + 'mode': { + allowed: ['string'], + allowed_data: ['detached', 'clearsign'] + // TODO 'opaque' is not used, but available on native app + }, + 'base64': { + allowed: ['boolean'] + }, + 'armor': { + allowed: ['boolean'] + }, + }, + answer: { + type: ['signature', 'ciphertext'], + payload: { + 'data': 'string', + }, + info: { + 'base64':'boolean' + } + } + }, + + // note: For the meaning of the optional keylist flags, refer to + // https://www.gnupg.org/documentation/manuals/gpgme/Key-Listing-Mode.html + keylist:{ + required: {}, + + optional: { + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + 'secret': { + allowed: ['boolean'] + }, + 'extern': { + allowed: ['boolean'] + }, + 'local':{ + allowed: ['boolean'] + }, + 'locate': { + allowed: ['boolean'] + }, + 'sigs':{ + allowed: ['boolean'] + }, + 'notations':{ + allowed: ['boolean'] + }, + 'tofu': { + allowed: ['boolean'] + }, + 'ephemeral': { + allowed: ['boolean'] + }, + 'validate': { + allowed: ['boolean'] + }, + 'keys': { + allowed: ['string'], + array_allowed: true + } + }, + answer: { + type: ['keys'], + info: { + 'keys': 'object', + 'base64': 'boolean', + } + } + }, + + export: { + required: {}, + optional: { + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + 'keys': { + allowed: ['string'], + array_allowed: true + }, + 'armor': { + allowed: ['boolean'] + }, + 'extern': { + allowed: ['boolean'] + }, + 'minimal': { + allowed: ['boolean'] + }, + 'raw': { + allowed: ['boolean'] + }, + 'pkcs12': { + allowed: ['boolean'] + }, + 'with-sec-fprs': { + allowed: ['boolean'] + } + // secret: not yet implemented + }, + answer: { + type: ['keys'], + payload: { + 'data': 'string', + }, + info: { + 'base64': 'boolean', + 'sec-fprs': 'object' + } + } + }, + + import: { + required: { + 'data': { + allowed: ['string'] + } + }, + optional: { + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + 'base64': { + allowed: ['boolean'] + }, + }, + answer: { + type: [], + info: { + 'result': 'object' + } + } + }, + + delete: { + pinentry: true, + required:{ + 'key': { + allowed: ['string'] + } + }, + optional: { + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + }, + answer: { + info: { + 'success': 'boolean' + } + } + }, + + version: { + required: {}, + optional: {}, + answer: { + type: [''], + info: { + 'gpgme': 'string', + 'info': 'object' + } + } + }, + + createkey: { + pinentry: true, + required: { + userid: { + allowed: ['string'] + } + }, + optional: { + algo: { + allowed: ['string'] + }, + expires: { + allowed: ['number'], + } + }, + answer: { + type: [''], + info: { 'fingerprint': 'string' } + } + }, + + verify: { + required: { + data: { + allowed: ['string'] + } + }, + optional: { + 'protocol': { + allowed: ['string'], + allowed_data: ['cms', 'openpgp'] + }, + 'signature': { + allowed: ['string'] + }, + 'base64':{ + allowed: ['boolean'] + } + }, + answer: { + type: ['plaintext'], + payload:{ + 'data': 'string' + }, + info: { + 'base64':'boolean', + 'info': 'object' + // info.file_name: Optional string of the plaintext file name. + // info.is_mime: Boolean if the messages claims it is MIME. + // info.signatures: Array of signatures + } + } + }, + + config_opt: { + required: { + 'component':{ + allowed: ['string'], + // allowed_data: ['gpg'] // TODO check all available + }, + 'option': { + allowed: ['string'], + // allowed_data: ['default-key'] // TODO check all available + } + }, + optional: {}, + answer: { + type: [], + info: { + 'option': 'object' + } + } + } +}; diff --git a/lang/js/unittest_inputvalues.js b/lang/js/unittest_inputvalues.js new file mode 100644 index 0000000..659ef85 --- /dev/null +++ b/lang/js/unittest_inputvalues.js @@ -0,0 +1,123 @@ +import { createKey } from './src/Key'; + +export const helper_params = { + validLongId: '0A0A0A0A0A0A0A0A', + validKeys: ['A1E3BC45BDC8E87B74F4392D53B151A1368E50F3', + createKey('D41735B91236FDB882048C5A2301635EEFF0CB05'), + 'EE17AEE730F88F1DE7713C54BBE0A4FF7851650A'], + validFingerprint: '9A9A7A7A8A9A9A7A7A8A9A9A7A7A8A9A9A7A7A8A', + validFingerprints: ['9A9A7A7A8A9A9A7A7A8A9A9A7A7A8A9A9A7A7A8A', + '9AAE7A338A9A9A7A7A8A9A9A7A7A8A9A9A7A7DDA'], + invalidLongId: '9A9A7A7A8A9A9A7A7A8A', + invalidFingerprints: [{ hello:'World' }, ['kekekeke'], new Uint32Array(40)], + invalidKeyArray: { curiosity:'uncat' }, + invalidKeyArray_OneBad: [ + createKey('D41735B91236FDB882048C5A2301635EEFF0CB05'), + 'E1D18E6E994FA9FE9360Bx0E687B940FEFEB095A', + '3AEA7FE4F5F416ED18CEC63DD519450D9C0FAEE5'], + invalidErrorCode: 'Please type in all your passwords.', + validGPGME_Key: createKey('D41735B91236FDB882048C5A2301635EEFF0CB05', true), + valid_openpgplike: { primaryKey: { + getFingerprint: function (){ + return '85DE2A8BA5A5AB3A8A7BE2000B8AED24D7534BC2';} + } + } +}; + +export const message_params = { + invalid_op_action : 'dance', + invalid_op_type : [234, 34, '<>'], + valid_encrypt_data: 'مرحبا بالعالم', + invalid_param_test: { + valid_op: 'encrypt', + invalid_param_names: [22,'dance', {}], + validparam_name_0: 'mime', + invalid_values_0: [2134, 'All your passwords', + createKey('12AE9F3E41B33BF77DF52B6BE8EE1992D7909B08'), null] + } +}; + +export const whatever_params = { + four_invalid_params: ['<(((-<', '>°;==;~~', '^^', '{{{{o}}}}'], +}; +export const key_params = { +// A Key you own (= having a secret Key) in GPG. See testkey.pub/testkey.sec + validKeyFingerprint: 'D41735B91236FDB882048C5A2301635EEFF0CB05', + // A Key you do not own (= having no secret Key) in GPG. See testkey2.pub + validFingerprintNoSecret: 'E059A1E0866D31AE131170884D9A2E13304153D1', + // A Key not in your Keyring. This is just a random hex string. + invalidKeyFingerprint: 'CDC3A2B2860625CCBFC5AAAAAC6D1B604967FC4A', + validKeyProperties: ['expired', 'disabled','invalid','can_encrypt', + 'can_sign','can_certify','can_authenticate','secret','is_qualified'] +}; +export const armoredKey = { + fingerprint: '78034948BA7F5D0E9BDB67E4F63790C11E60278A', + key:'-----BEGIN PGP PUBLIC KEY BLOCK-----\n' + + '\n' + + 'mQENBFsPvK0BCACaIgoIN+3g05mrTITULK/YDTrfg4W7RdzIZBxch5CM0zdu/dby\n' + + 'esFwaJbVQIqu54CRz5xKAiWmRrQCaRvhvjY0na5r5UUIpbeQiOVrl65JtNbRmlik\n' + + 'd9Prn1kZDUOZiCPIKn+/M2ecJ92YedM7I4/BbpiaFB11cVrPFg4thepn0LB3+Whp\n' + + '9HDm4orH9rjy6IUr6yjWNIr+LYRY6/Ip2vWcMVjleEpTFznXrm83hrJ0n0INtyox\n' + + 'Nass4eDWkgo6ItxDFFLOORSmpfrToxZymSosWqgux/qG6sxHvLqlqy6Xe3ZYRFbG\n' + + '+JcA1oGdwOg/c0ndr6BYYiXTh8+uUJfEoZvzABEBAAG0HEJsYSBCbGEgPGJsYWJs\n' + + 'YUBleGFtcGxlLm9yZz6JAVQEEwEIAD4WIQR4A0lIun9dDpvbZ+T2N5DBHmAnigUC\n' + + 'Ww+8rQIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD2N5DBHmAn\n' + + 'igwIB/9K3E3Yev9taZP4KnXPhk1oMQRW1MWAsFGUr+70N85VwedpUawymW4vXi1+\n' + + 'hMeTc39QjmZ0+VqHkJttkqEN6bLcEvgmU/mOlOgKdzy6eUcasYAzgoAKUqSX1SPs\n' + + '0Imo7Tj04wnfnVwvKxaeadi0VmdqIYaW75UlrzIaltsBctyeYH8sBrvaTLscb4ON\n' + + '46OM3Yw2G9+dBF0P+4UYFHP3EYZMlzNxfwF+i2HsYcNDHlcLfjENr9GwKn5FJqpY\n' + + 'Iq3qmI37w1hVasHDxXdz1X06dpsa6Im4ACk6LXa7xIQlXxTgPAQV0sz2yB5eY+Md\n' + + 'uzEXPGW+sq0WRp3hynn7kVP6QQYvuQENBFsPvK0BCACwvBcmbnGJk8XhEBRu2QN3\n' + + 'jKgVs3CG5nE2Xh20JipZwAuGHugDLv6/jlizzz5jtj3SAHVtJB8lJW8I0cNSEIX8\n' + + 'bRYH4C7lP2DTb9CgMcGErQIyK480+HIsbsZhJSNHdjUUl6IPEEVfSQzWaufmuswe\n' + + 'e+giqHiTsaiW20ytXilwVGpjlHBaxn/bpskZ0YRasgnPqKgJD3d5kunNqWoyCpMc\n' + + 'FYgDERvPbhhceFbvFE9G/u3gbcuV15mx53dDX0ImvPcvJnDOyJS9yr7ApdOV312p\n' + + 'A1MLbxfPnbnVu+dGXn7D/VCDd5aBYVPm+5ANrk6z9lYKH9aO5wgXpLAdJvutCOL5\n' + + 'ABEBAAGJATwEGAEIACYWIQR4A0lIun9dDpvbZ+T2N5DBHmAnigUCWw+8rQIbDAUJ\n' + + 'A8JnAAAKCRD2N5DBHmAnigMVB/484G2+3R0cAaj3V/z4gW3MRSMhcYqEMyJ/ACdo\n' + + '7y8eoreYW843JWWVDRY6/YcYYGuBBP47WO4JuP2wIlVn17XOCSgnNjmmjsIYiAzk\n' + + 'op772TB27o0VeiFX5iWcawy0EI7JCb23xpI+QP31ksL2yyRYFXCtXSUfcOrLpCY8\n' + + 'aEQMQbAGtkag1wHTo/Tf/Vip8q0ZEQ4xOKTR2/ll6+inP8kzGyzadElUnH1Q1OUX\n' + + 'd2Lj/7BpBHE2++hAjBQRgnyaONF7mpUNEuw64iBNs0Ce6Ki4RV2+EBLnFubnFNRx\n' + + 'fFJcYXcijhuf3YCdWzqYmPpU/CtF4TgDlfSsdxHxVOmnZkY3\n' + + '=qP6s\n' + + '-----END PGP PUBLIC KEY BLOCK-----\n', + keyChangedUserId: '-----BEGIN PGP PUBLIC KEY BLOCK-----\n' + + '\n' + + 'mQENBFsPvK0BCACaIgoIN+3g05mrTITULK/YDTrfg4W7RdzIZBxch5CM0zdu/dby\n' + + 'esFwaJbVQIqu54CRz5xKAiWmRrQCaRvhvjY0na5r5UUIpbeQiOVrl65JtNbRmlik\n' + + 'd9Prn1kZDUOZiCPIKn+/M2ecJ92YedM7I4/BbpiaFB11cVrPFg4thepn0LB3+Whp\n' + + '9HDm4orH9rjy6IUr6yjWNIr+LYRY6/Ip2vWcMVjleEpTFznXrm83hrJ0n0INtyox\n' + + 'Nass4eDWkgo6ItxDFFLOORSmpfrToxZymSosWqgux/qG6sxHvLqlqy6Xe3ZYRFbG\n' + + '+JcA1oGdwOg/c0ndr6BYYiXTh8+uUJfEoZvzABEBAAG0HEJsYSBCbGEgPGJsYWJs\n' + + 'YUBleGFtcGxlLm9yZz6JAVQEEwEIAD4WIQR4A0lIun9dDpvbZ+T2N5DBHmAnigUC\n' + + 'Ww+8rQIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD2N5DBHmAn\n' + + 'igwIB/9K3E3Yev9taZP4KnXPhk1oMQRW1MWAsFGUr+70N85VwedpUawymW4vXi1+\n' + + 'hMeTc39QjmZ0+VqHkJttkqEN6bLcEvgmU/mOlOgKdzy6eUcasYAzgoAKUqSX1SPs\n' + + '0Imo7Tj04wnfnVwvKxaeadi0VmdqIYaW75UlrzIaltsBctyeYH8sBrvaTLscb4ON\n' + + '46OM3Yw2G9+dBF0P+4UYFHP3EYZMlzNxfwF+i2HsYcNDHlcLfjENr9GwKn5FJqpY\n' + + 'Iq3qmI37w1hVasHDxXdz1X06dpsa6Im4ACk6LXa7xIQlXxTgPAQV0sz2yB5eY+Md\n' + + 'uzEXPGW+sq0WRp3hynn7kVP6QQYvtCZTb21lb25lIEVsc2UgPHNvbWVvbmVlbHNl\n' + + 'QGV4YW1wbGUub3JnPokBVAQTAQgAPhYhBHgDSUi6f10Om9tn5PY3kMEeYCeKBQJb\n' + + 'D705AhsDBQkDwmcABQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEPY3kMEeYCeK\n' + + 'aIUH/2o+Ra+GzxgZrVexXLL+FCSmcu0cxeWfMhL8jd96c6uXIT21qQMRU2jgvnUp\n' + + 'Wdi/BeLKp5lYwywm04PFhmRVxWXLuLArCsDu+CFys+aPeybnjikPBZov6P8/cZV3\n' + + 'cd6zxFvqB9J15HjDMcl/r5v6d4CgSLKlFebrO5WKxHa6zGK9TRMQrqTu1heKHRf6\n' + + '4+Wj+MZmYnPzEQePjiBw/VkJ1Nm37Dd24gKdcN/qJFwEOqvbI5RIjB7xqoDslZk9\n' + + 'sAivBXwF0E9HKqvh4WZZeA7uaWNdGo/cQkD5rab5SdHGNPHLbzoRWScsM8WYtsME\n' + + 'dEMp5iPuG9M63+TD7losAkJ/TlS5AQ0EWw+8rQEIALC8FyZucYmTxeEQFG7ZA3eM\n' + + 'qBWzcIbmcTZeHbQmKlnAC4Ye6AMu/r+OWLPPPmO2PdIAdW0kHyUlbwjRw1IQhfxt\n' + + 'FgfgLuU/YNNv0KAxwYStAjIrjzT4cixuxmElI0d2NRSXog8QRV9JDNZq5+a6zB57\n' + + '6CKoeJOxqJbbTK1eKXBUamOUcFrGf9umyRnRhFqyCc+oqAkPd3mS6c2pajIKkxwV\n' + + 'iAMRG89uGFx4Vu8UT0b+7eBty5XXmbHnd0NfQia89y8mcM7IlL3KvsCl05XfXakD\n' + + 'UwtvF8+dudW750ZefsP9UIN3loFhU+b7kA2uTrP2Vgof1o7nCBeksB0m+60I4vkA\n' + + 'EQEAAYkBPAQYAQgAJhYhBHgDSUi6f10Om9tn5PY3kMEeYCeKBQJbD7ytAhsMBQkD\n' + + 'wmcAAAoJEPY3kMEeYCeKAxUH/jzgbb7dHRwBqPdX/PiBbcxFIyFxioQzIn8AJ2jv\n' + + 'Lx6it5hbzjclZZUNFjr9hxhga4EE/jtY7gm4/bAiVWfXtc4JKCc2OaaOwhiIDOSi\n' + + 'nvvZMHbujRV6IVfmJZxrDLQQjskJvbfGkj5A/fWSwvbLJFgVcK1dJR9w6sukJjxo\n' + + 'RAxBsAa2RqDXAdOj9N/9WKnyrRkRDjE4pNHb+WXr6Kc/yTMbLNp0SVScfVDU5Rd3\n' + + 'YuP/sGkEcTb76ECMFBGCfJo40XualQ0S7DriIE2zQJ7oqLhFXb4QEucW5ucU1HF8\n' + + 'UlxhdyKOG5/dgJ1bOpiY+lT8K0XhOAOV9Kx3EfFU6admRjc=\n' + + '=9WZ7\n' + + '-----END PGP PUBLIC KEY BLOCK-----\n' +};
\ No newline at end of file diff --git a/lang/js/unittests.js b/lang/js/unittests.js new file mode 100644 index 0000000..f28be76 --- /dev/null +++ b/lang/js/unittests.js @@ -0,0 +1,386 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + */ + +import './node_modules/mocha/mocha'; /* global mocha, it, describe*/ +import './node_modules/chai/chai';/* global chai*/ +import { helper_params as hp } from './unittest_inputvalues'; +import { message_params as mp } from './unittest_inputvalues'; +import { whatever_params as wp } from './unittest_inputvalues'; +import { key_params as kp } from './unittest_inputvalues'; +import { Connection } from './src/Connection'; +import { gpgme_error, err_list } from './src/Errors'; +import { toKeyIdArray , isFingerprint } from './src/Helpers'; +import { createKey } from './src/Key'; +import { GPGME_Keyring } from './src/Keyring'; +import { GPGME_Message, createMessage } from './src/Message'; + +mocha.setup('bdd'); +const expect = chai.expect; +chai.config.includeStack = true; +const connectionTimeout = 2000; + +function unittests (){ + describe('Connection testing', function (){ + + it('Connecting', function (done) { + let conn0 = new Connection; + conn0.checkConnection(true, connectionTimeout).then( + function (answer) { + expect(answer).to.not.be.empty; + expect(answer.gpgme).to.not.be.undefined; + expect(answer.gpgme).to.be.a('string'); + expect(answer.info).to.be.an('Array'); + expect(conn0.disconnect).to.be.a('function'); + expect(conn0.post).to.be.a('function'); + done(); + }); + + }); + + it('Disconnecting', function (done) { + let conn0 = new Connection; + conn0.checkConnection(false, connectionTimeout).then( + function (answer) { + expect(answer).to.be.true; + conn0.disconnect(); + conn0.checkConnection(false, connectionTimeout).then( + function (result) { + expect(result).to.be.false; + done(); + }); + }); + }); + }); + + describe('Error Object handling', function (){ + // TODO: new GPGME_Error codes + it('check the Timeout error', function (){ + let test0 = gpgme_error('CONN_TIMEOUT'); + + expect(test0).to.be.an.instanceof(Error); + expect(test0.code).to.equal('CONN_TIMEOUT'); + }); + + it('Error Object returns generic code if code is not listed', + function (){ + let test0 = gpgme_error(hp.invalidErrorCode); + + expect(test0).to.be.an.instanceof(Error); + expect(test0.code).to.equal('GENERIC_ERROR'); + } + ); + + it('Warnings like PARAM_IGNORED should not return errors', function (){ + let test0 = gpgme_error('PARAM_IGNORED'); + + expect(test0).to.be.null; + }); + }); + + describe('Fingerprint checking', function (){ + + it('isFingerprint(): valid Fingerprint', function (){ + let test0 = isFingerprint(hp.validFingerprint); + + expect(test0).to.be.true; + }); + + it('isFingerprint(): invalid Fingerprints', function (){ + for (let i=0; i < hp.invalidFingerprints.length; i++){ + let test0 = isFingerprint(hp.invalidFingerprints[i]); + + expect(test0).to.be.false; + } + }); + }); + + describe('toKeyIdArray() (converting input to fingerprint)', function (){ + + it('Correct fingerprint string', function (){ + let test0 = toKeyIdArray(hp.validFingerprint); + + expect(test0).to.be.an('array'); + expect(test0).to.include(hp.validFingerprint); + }); + + it('openpgpjs-like object', function (){ + let test0 = toKeyIdArray(hp.valid_openpgplike); + + expect(test0).to.be.an('array').with.lengthOf(1); + expect(test0).to.include( + hp.valid_openpgplike.primaryKey.getFingerprint()); + }); + + it('Array of valid inputs', function (){ + let test0 = toKeyIdArray(hp.validKeys); + expect(test0).to.be.an('array'); + expect(test0).to.have.lengthOf(hp.validKeys.length); + }); + + it('Incorrect inputs', function (){ + + it('valid Long ID', function (){ + let test0 = toKeyIdArray(hp.validLongId); + + expect(test0).to.be.empty; + }); + + it('invalidFingerprint', function (){ + let test0 = toKeyIdArray(hp.invalidFingerprint); + + expect(test0).to.be.empty; + }); + + it('invalidKeyArray', function (){ + let test0 = toKeyIdArray(hp.invalidKeyArray); + + expect(test0).to.be.empty; + }); + + it('Partially invalid array', function (){ + let test0 = toKeyIdArray(hp.invalidKeyArray_OneBad); + + expect(test0).to.be.an('array'); + expect(test0).to.have.lengthOf( + hp.invalidKeyArray_OneBad.length - 1); + }); + }); + }); + + describe('GPGME_Key', function (){ + it('Key has data after a first refresh', function (done) { + let key = createKey(kp.validKeyFingerprint); + key.refreshKey().then(function (key2){ + expect(key2.get).to.be.a('function'); + for (let i=0; i < kp.validKeyProperties.length; i++) { + let prop = key2.get(kp.validKeyProperties[i]); + expect(prop).to.not.be.undefined; + expect(prop).to.be.a('boolean'); + } + expect(isFingerprint(key2.get('fingerprint'))).to.be.true; + expect( + key2.get('fingerprint')).to.equal(kp.validKeyFingerprint); + expect( + key2.get('fingerprint')).to.equal(key.fingerprint); + done(); + }); + }); + + it('Non-cached key async data retrieval', function (done){ + let key = createKey(kp.validKeyFingerprint, true); + key.get('can_authenticate').then(function (result){ + expect(result).to.be.a('boolean'); + done(); + }); + }); + + it('Non-cached key async armored Key', function (done){ + let key = createKey(kp.validKeyFingerprint, true); + key.get('armored').then(function (result){ + expect(result).to.be.a('string'); + expect(result).to.include('KEY BLOCK-----'); + done(); + }); + }); + + it('Non-cached key async hasSecret', function (done){ + let key = createKey(kp.validKeyFingerprint, true); + key.get('hasSecret').then(function (result){ + expect(result).to.be.a('boolean'); + done(); + }); + }); + + it('Non-cached key async hasSecret (no secret in Key)', function (done){ + let key = createKey(kp.validFingerprintNoSecret, true); + key.get('hasSecret').then(function (result){ + expect(result).to.be.a('boolean'); + expect(result).to.equal(false); + done(); + }); + }); + + it('Querying non-existing Key returns an error', function (done) { + let key = createKey(kp.invalidKeyFingerprint); + key.refreshKey().then(function (){}, + function (error){ + expect(error).to.be.an.instanceof(Error); + expect(error.code).to.equal('KEY_NOKEY'); + done(); + }); + }); + + it('createKey returns error if parameters are wrong', function (){ + for (let i=0; i< 4; i++){ + expect(function (){ + createKey(wp.four_invalid_params[i]); + }).to.throw( + err_list.PARAM_WRONG.msg + ); + + } + }); + + // it('Overwriting getFingerprint does not work', function(){ + // const evilFunction = function(){ + // return 'bad Data'; + // }; + // let key = createKey(kp.validKeyFingerprint, true); + // expect(key.fingerprint).to.equal(kp.validKeyFingerprint); + // try { + // key.getFingerprint = evilFunction; + // } + // catch(e) { + // expect(e).to.be.an.instanceof(TypeError); + // } + // expect(key.fingerprint).to.equal(kp.validKeyFingerprint); + // expect(key.getFingerprint).to.not.equal(evilFunction); + // }); + }); + + describe('GPGME_Keyring', function (){ + + it('correct Keyring initialization', function (){ + let keyring = new GPGME_Keyring; + expect(keyring).to.be.an.instanceof(GPGME_Keyring); + expect(keyring.getKeys).to.be.a('function'); + }); + + it('Loading Keys from Keyring, to be used synchronously', + function (done){ + let keyring = new GPGME_Keyring; + keyring.getKeys({ prepare_sync: true }).then(function (result){ + expect(result).to.be.an('array'); + expect(result[0].get('hasSecret')).to.be.a('boolean'); + done(); + }); + } + ); + + it('Loading specific Key from Keyring, to be used synchronously', + function (done){ + let keyring = new GPGME_Keyring; + keyring.getKeys({ + pattern: kp.validKeyFingerprint, + prepare_sync: true }).then( + function (result){ + expect(result).to.be.an('array'); + expect(result[0].get('hasSecret')).to.be.a('boolean'); + done(); + } + ); + } + ); + + it('Querying non-existing Key from Keyring', function (done){ + let keyring = new GPGME_Keyring; + keyring.getKeys({ + pattern: kp.invalidKeyFingerprint, + prepare_sync: true + }).then(function (result){ + expect(result).to.be.an('array'); + expect(result.length).to.equal(0); + done(); + }); + }); + + }); + + describe('GPGME_Message', function (){ + + it('creating encrypt Message', function (){ + let test0 = createMessage('encrypt'); + + expect(test0).to.be.an.instanceof(GPGME_Message); + expect(test0.isComplete()).to.be.false; + }); + + it('Message is complete after setting mandatory data', function (){ + let test0 = createMessage('encrypt'); + test0.setParameter('data', mp.valid_encrypt_data); + test0.setParameter('keys', hp.validFingerprints); + + expect(test0.isComplete()).to.be.true; + }); + + it('Message is not complete after mandatory data is empty', function (){ + let test0 = createMessage('encrypt'); + test0.setParameter('keys', hp.validFingerprints); + expect(test0.isComplete()).to.be.false; + expect(function (){ + test0.setParameter('data', ''); + }).to.throw( + err_list.PARAM_WRONG.msg); + }); + + it('Complete Message contains the data that was set', function (){ + let test0 = createMessage('encrypt'); + test0.setParameter('data', mp.valid_encrypt_data); + test0.setParameter('keys', hp.validFingerprints); + + expect(test0.message).to.not.be.null; + expect(test0.message).to.have.keys('op', 'data', 'keys', + 'chunksize'); + expect(test0.message.op).to.equal('encrypt'); + expect(test0.message.data).to.equal( + mp.valid_encrypt_data); + }); + + it ('Not accepting non-allowed operation', function (){ + expect(function () { + createMessage(mp.invalid_op_action); + }).to.throw( + err_list.MSG_WRONG_OP.msg); + }); + it('Not accepting wrong parameter type', function (){ + expect(function () { + createMessage(mp.invalid_op_type); + }).to.throw( + err_list.PARAM_WRONG.msg); + }); + + it('Not accepting wrong parameter name', function (){ + let test0 = createMessage(mp.invalid_param_test.valid_op); + for (let i=0; + i < mp.invalid_param_test.invalid_param_names.length; i++){ + expect(function (){ + test0.setParameter( + mp.invalid_param_test.invalid_param_names[i], + 'Somevalue');} + ).to.throw(err_list.PARAM_WRONG.msg); + } + }); + + it('Not accepting wrong parameter value', function (){ + let test0 = createMessage(mp.invalid_param_test.valid_op); + for (let j=0; + j < mp.invalid_param_test.invalid_values_0.length; j++){ + expect(function (){ + test0.setParameter( + mp.invalid_param_test.validparam_name_0, + mp.invalid_param_test.invalid_values_0[j]); + }).to.throw(err_list.PARAM_WRONG.msg); + } + }); + }); + +} + +export default { unittests };
\ No newline at end of file diff --git a/lang/js/webpack.conf.js b/lang/js/webpack.conf.js new file mode 100644 index 0000000..19f3bbd --- /dev/null +++ b/lang/js/webpack.conf.js @@ -0,0 +1,36 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * This is the configuration file for building the gpgmejs-Library with webpack + */ +/* global require, module, __dirname */ +const path = require('path'); + +module.exports = { + entry: './src/index.js', + // mode: 'development', + mode: 'production', + output: { + path: path.resolve(__dirname, 'build'), + filename: 'gpgmejs.bundle.js', + libraryTarget: 'var', + libraryExport: 'default', + library: 'Gpgmejs' + } +}; diff --git a/lang/js/webpack.conf_unittests.js b/lang/js/webpack.conf_unittests.js new file mode 100644 index 0000000..c3c87f3 --- /dev/null +++ b/lang/js/webpack.conf_unittests.js @@ -0,0 +1,36 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + * + * This is the configuration file for building the gpgmejs-Library with webpack + */ +/* global require, module, __dirname */ + +const path = require('path'); + +module.exports = { + entry: './unittests.js', + mode: 'production', + output: { + path: path.resolve(__dirname, 'build'), + filename: 'gpgmejs_unittests.bundle.js', + libraryTarget: 'var', + libraryExport: 'default', + library: 'Gpgmejs_test' + } +}; diff --git a/lang/python/MANIFEST.in b/lang/python/MANIFEST.in index c34e84a..f4def41 100644 --- a/lang/python/MANIFEST.in +++ b/lang/python/MANIFEST.in @@ -1,4 +1,10 @@ -recursive-include examples *.py +recursive-include doc *.org +recursive-include doc *.rst +recursive-include doc *.tex +recursive-include doc *.texi +recursive-include doc *.info +recursive-include examples *.py *.pyx +include README README.org include gpgme.i include helpers.c helpers.h private.h include version.py diff --git a/lang/python/Makefile.am b/lang/python/Makefile.am index 8d74cbd..6988faf 100644 --- a/lang/python/Makefile.am +++ b/lang/python/Makefile.am @@ -22,6 +22,7 @@ EXTRA_DIST = \ gpgme.i \ helpers.c helpers.h private.h \ examples \ + doc \ src SUBDIRS = . tests @@ -33,7 +34,7 @@ prepare: copystamp # distutils are not VPATH-aware. copystamp: ln -sf "$(top_srcdir)/src/data.h" . - ln -sf "$(top_builddir)/config.h" . + ln -sf "$(top_builddir)/conf/config.h" . ln -sf "$(srcdir)/src" gpg touch $@ @@ -54,7 +55,8 @@ python$(PYTHON_VERSION)-gpg/dist/gpg-$(VERSION).tar.gz.asc: copystamp top_builddir="$(top_builddir)" \ $(PYTHON) setup.py sdist --verbose --dist-dir=python$(PYTHON_VERSION)-gpg-dist \ --manifest=python$(PYTHON_VERSION)-gpg-dist/MANIFEST - gpg2 --detach-sign --armor python$(PYTHON_VERSION)-gpg-dist/gpg-$(VERSION).tar.gz + gpgbin=gpgconf --list-components | grep OpenPGP | sed -e 's/gpg:OpenPGP://g' + $(gpgbin) --detach-sign --armor python$(PYTHON_VERSION)-gpg-dist/gpg-$(VERSION).tar.gz .PHONY: sdist sdist: python$(PYTHON_VERSION)-gpg/dist/gpg-$(VERSION).tar.gz.asc diff --git a/lang/python/Makefile.in b/lang/python/Makefile.in index ae0334f..95d6e7e 100644 --- a/lang/python/Makefile.in +++ b/lang/python/Makefile.in @@ -114,7 +114,7 @@ am__aclocal_m4_deps = $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ $(ACLOCAL_M4) mkinstalldirs = $(SHELL) $(top_srcdir)/build-aux/mkinstalldirs -CONFIG_HEADER = $(top_builddir)/config.h +CONFIG_HEADER = $(top_builddir)/conf/config.h CONFIG_CLEAN_FILES = version.py setup.py CONFIG_CLEAN_VPATH_FILES = AM_V_P = $(am__v_P_@AM_V@) @@ -398,6 +398,7 @@ EXTRA_DIST = \ gpgme.i \ helpers.c helpers.h private.h \ examples \ + doc \ src SUBDIRS = . tests @@ -730,7 +731,7 @@ prepare: copystamp # distutils are not VPATH-aware. copystamp: ln -sf "$(top_srcdir)/src/data.h" . - ln -sf "$(top_builddir)/config.h" . + ln -sf "$(top_builddir)/conf/config.h" . ln -sf "$(srcdir)/src" gpg touch $@ @@ -751,7 +752,8 @@ python$(PYTHON_VERSION)-gpg/dist/gpg-$(VERSION).tar.gz.asc: copystamp top_builddir="$(top_builddir)" \ $(PYTHON) setup.py sdist --verbose --dist-dir=python$(PYTHON_VERSION)-gpg-dist \ --manifest=python$(PYTHON_VERSION)-gpg-dist/MANIFEST - gpg2 --detach-sign --armor python$(PYTHON_VERSION)-gpg-dist/gpg-$(VERSION).tar.gz + gpgbin=gpgconf --list-components | grep OpenPGP | sed -e 's/gpg:OpenPGP://g' + $(gpgbin) --detach-sign --armor python$(PYTHON_VERSION)-gpg-dist/gpg-$(VERSION).tar.gz .PHONY: sdist sdist: python$(PYTHON_VERSION)-gpg/dist/gpg-$(VERSION).tar.gz.asc diff --git a/lang/python/README b/lang/python/README index 99da4dd..a13345f 100644 --- a/lang/python/README +++ b/lang/python/README @@ -13,7 +13,7 @@ Table of Contents The "gpg" module is a python interface to the GPGME library: -[https://www.gnupg.org/software/gpgme/] +<https://www.gnupg.org/software/gpgme/> "gpg" offers two interfaces, one is a high-level, curated, and idiomatic interface that is implemented as a shim on top of the low-level @@ -27,16 +27,16 @@ functionality of the underlying library. ══════════════ For general discussion and help see the gnupg-users mailing list: - [https://lists.gnupg.org/mailman/listinfo/gnupg-users] + <https://lists.gnupg.org/mailman/listinfo/gnupg-users> For development see the gnupg-devel mailing list: - [https://lists.gnupg.org/mailman/listinfo/gnupg-devel] + <https://lists.gnupg.org/mailman/listinfo/gnupg-devel> 2 Bugs ══════ - Please report bugs using our bug tracker [https://bugs.gnupg.org] with + Please report bugs using our bug tracker <https://bugs.gnupg.org> with tag (aka project) 'gpgme'. @@ -44,8 +44,8 @@ functionality of the underlying library. ═════════ PyME was created by John Goerzen, and maintained, developed, and - cherished by Igor Belyi, Martin Albrecht, Ben McGinnes, and everyone - who contributed to it in any way. + cherished by Igor Belyi, Martin Albrecht, Ben McGinnes, Justus + Winter, and everyone who contributed to it in any way. In 2016 we merged a port of PyME to into the GPGME repository, and development will continue there. Please see the VCS history for the @@ -64,14 +64,14 @@ functionality of the underlying library. • The bindings have been merged into the GPGME repository in 2016. • The latest version of PyME for Python 3.2 and above (as of May, - 2015) is v0.9.1. [https://git.gnupg.org/gpgme.git/lang/py3-pyme] + 2015) is v0.9.1. <https://git.gnupg.org/gpgme.git/lang/py3-pyme> • The latest version of PyME for Python 2.6 and 2.7 (as of this - writing) is v0.9.0. [https://bitbucket.org/malb/pyme] + writing) is v0.9.0. <https://bitbucket.org/malb/pyme> • A previous version of PyME v0.8.0 can be found on sourceforge: - [http://pyme.sourceforge.net/] + <http://pyme.sourceforge.net/> • A previous version of PyME v0.5.1 which works with GPGME v0.3.15 can - be found on John Goerzen's PyME page: [http://quux.org/devel/pyme/] - [http://www.complete.org/JohnGoerzen] + be found on John Goerzen's PyME page: <http://quux.org/devel/pyme/> + <http://www.complete.org/JohnGoerzen> diff --git a/lang/python/doc/README b/lang/python/doc/README new file mode 100644 index 0000000..a14e1ad --- /dev/null +++ b/lang/python/doc/README @@ -0,0 +1,47 @@ +GPGME Python Bindings Documentation +=================================== + +As the GPGME Python bindings exist in two worlds within the FOSS +universe, it's always had a little issue with regards to its +documentation and specifically to the format of it. The GnuPG +Project, like much of the rest of the GNU Project, uses Texinfo to +build its documentation. While the actual format used to write and +edit that documentation is Org mode. Largely because most, if not +all, of the GnuPG developers use GNU Emacs for much of their work. + +The Python world, however, utilises reStructuredText almost +universally. This in turn is used by Sphinx or Docutils directly to +build the documentation. + +Each has various advantages for their own ecisystems, but this part of +the GnuPG effort is aimed at both sides. So, long story short, this +documentation is provided as both Texinfo and reStructuredText files. + +This docs directory contains four main subdirectories: + + 1. meta + 2. src + 3. rst + 4. texinfo + +The Meta directory is for docs that are not intended for distribution +or are about the docs themselves. The sole exception being this RDME +file. + +The Src directory is where the original edited files are, from which +the following two formats are generated initially. Most, if not all, +of these are written in Org Mode. + +The ReST directory contains reStructuredText files ehich have been +converted to that format from the Org Mode files via Pandoc. + +The Texinfo directory contains Texinfo files which have been exported +to that format from the Org Mode files by Org Mode itself within GNU +Emacs. + +Those latter two directories should then be used by their respective +build systems to produce the various output file formats they normally +do. They should not spill out into this parent directory. +Particularly since it is quite possible, perhaps even likely, that +alternatives to both of them may be added to this parent documentation +directory at some future point. diff --git a/lang/python/doc/meta/TODO.org b/lang/python/doc/meta/TODO.org new file mode 100644 index 0000000..0be99b3 --- /dev/null +++ b/lang/python/doc/meta/TODO.org @@ -0,0 +1,251 @@ +#+TITLE: Stuff To Do +#+LATEX_COMPILER: xelatex +#+LATEX_CLASS: article +#+LATEX_CLASS_OPTIONS: [12pt] +#+LATEX_HEADER: \usepackage{xltxtra} +#+LATEX_HEADER: \usepackage[margin=1in]{geometry} +#+LATEX_HEADER: \setmainfont[Ligatures={Common}]{Latin Modern Roman} + +* Project Task List + :PROPERTIES: + :CUSTOM_ID: task-list + :END: + +** DONE Documentation default format + CLOSED: [2018-02-15 Thu 21:29] + :PROPERTIES: + :CUSTOM_ID: todo-docs-default + :END: + + Decide on a default file format for documentation. The two main + contenders being Org Mode, the default for the GnuPG Project and + reStructuredText, the default for Python projects. A third option + of DITA XML was considered due to a number of beneficial features + it provides. + + The decision was made to use Org Mode in order to fully integrate + with the rest of the GPGME and GnuPG documentation. It is possible + to produce reST versions via Pandoc and DITA XML can be reached + through converting to either Markdown or XHTML first. + + +** TODO Documentation build systems + :PROPERTIES: + :CUSTOM_ID: todo-docs-build-systems + :END: + +Though Org Mode is being used for the default documentation format, it +still needs to end up as usable by end users. So the Org Mode files +are used to produce the "source" files used by the two main contenders +for documenting the bindings: Texinfo and ReStructuredText/Docutils. + + +*** TODO Texinfo documentation + :PROPERTIES: + :CUSTOM_ID: todo-docs-build-texinfo + :END: + +Need to add all of Texinfo's ... special systems to make it do its +things. + + +*** TODO ReStructuredText documentation + :PROPERTIES: + :CUSTOM_ID: todo-docs-build-docutils + :END: + +Need to run Sphinx's quick start, add it to the requirements and tweak +the index page for the rst files to point to the HOWTO and other files. + +It might just be easier to do all that in Org Mode and convert the +lot, then the Sphinx bits can be automated. + + +** STARTED Documentation HOWTO + :PROPERTIES: + :CUSTOM_ID: todo-docs-howto + :END: + + - State "STARTED" from "TODO" [2018-03-08 Thu 13:59] \\ + Started yesterday. + Write a HOWTO style guide for the current Python bindings. + +*** DONE Start python bindings HOWTO + CLOSED: [2018-03-07 Wed 18:14] + :PROPERTIES: + :CUSTOM_ID: howto-start + :END: + + +*** STARTED Include certain specific instructions in the HOWTO + :PROPERTIES: + :CUSTOM_ID: howto-requests + :END: + + Note: moved the S/MIME bits out to their own section of the TODO + list and may be served better by separate HOWTO documentation + anyway. + + - State "STARTED" from "TODO" [2018-03-09 Fri 15:27] + Some functions can be worked out from the handful of examples + available, but many more can't and I've already begun receiving + requests for certain functions to be explained. + + +**** DONE Standard scenarios + CLOSED: [2018-03-19 Mon 12:34] + :PROPERTIES: + :CUSTOM_ID: howto-the-basics + :END: + + - State "DONE" from "STARTED" [2018-03-19 Mon 12:34] \\ + All four of those are done. + - State "STARTED" from "TODO" [2018-03-09 Fri 15:26] \\ + Began with the example code, now to add the text. + What everyone expects: encryption, decryption, signing and verifying. + + +**** STARTED Key control + :PROPERTIES: + :CUSTOM_ID: howto-key-control + :END: + + - State "STARTED" from "TODO" [2018-03-19 Mon 12:35] \\ + Generating keys and subkeys are done, but revocation is still to be done. + Generating keys, adding subkeys, revoking subkeys (and keeping + the cert key), adding and revoking UIDs, signing/certifying keys. + + +**** DONE More key control + CLOSED: [2018-03-19 Mon 12:36] + :PROPERTIES: + :CUSTOM_ID: howto-key-selection + :END: + + - State "DONE" from "TODO" [2018-03-19 Mon 12:36] \\ + Key selection, searching, matching and counting is done. + Selecting keys to encrypt to or manipulate in other ways (e.g. as + with key control or the basics). + + +** TODO Documentation SWIG + :PROPERTIES: + :CUSTOM_ID: todo-docs-swig + :END: + + Write documentation for the complete SWIG bindings demonstrating + the correspondence with GPGME itself. + + Note: it is likely that this will be more in the nature of + something to be used in conjunction with the existing GPGME + documentation which makes it easier for Python developers to use. + + +** TODO GUI examples + :PROPERTIES: + :CUSTOM_ID: todo-gui-examples + :END: + + Create some examples of using Python bindings in a GUI application + to either match or be similar to the old GTK2 examples available + with PyME. + + +** TODO Replace SWIG + :PROPERTIES: + :CUSTOM_ID: todo-replace-swig + :END: + + Selecting SWIG for this project in 2002 was understandable and + effectively the only viable option. The options available now, + however, are significantly improved and some of those would resolve + a number of existing problems with using SWIG, particularly when + running code on both POSIX compliant and Windows platforms. + + The long term goal is to replace SWIG by reimplementing the Python + bindings using a more suitable means of interfacing with the GPGME + C source code. + + +*** TODO Replacement for SWIG + :PROPERTIES: + :CUSTOM_ID: todo-replace-swig-replacement + :END: + + Decide on a replacement for SWIG. Currently CFFI is looking like + the most viable candidate, but some additional testing and checks + are yet to be completed. + + +** TODO API for an API + :PROPERTIES: + :CUSTOM_ID: todo-api-squared + :END: + + A C API like GPGME is not what most modern developers think of when + they hear the term API. Normally they think of something they can + interact with like a RESTful web API. Though RESTful is unlikely + given the nature of GPGME and the process of encryption, it may be + possible to provide a more familiar interface which can be utilised + by developers of other languages for which bindings are not + available or for which it is too difficult to create proper + bindings. + + +** TODO S/MIME + :PROPERTIES: + :CUSTOM_ID: s-mime + :END: + + Eventually add some of this, but the OpenPGP details are far more + important at the moment. + + +* Project Task Details + :PROPERTIES: + :CUSTOM_ID: detailed-tasks + :END: + +** Working examples + :PROPERTIES: + :CUSTOM_ID: working-examples + :END: + + The old GUI examples were unable to be retained since they depended + on GTK2 and Python 2's integration with GTK2. + + Current GPGME examples so far only include command line tools or + basic Python code for use with either Python 2.7 or Python 3.4 and + above. + + Future GUI examples ought to utilise available GUI modules and + libraries supported by Python 3. This may include Qt frameworks, + Tkinter, GTK3 or something else entirely. + +** Documentation + :PROPERTIES: + :CUSTOM_ID: documentation + :END: + + The legacy documentation which no longer applies to the Python + bindings has been removed. + + Current and future documentation will adhere to the GnuPG standard + of using Org Mode and not use the reStructuredText (reST) format + more commonly associated with Python documentation. The reasons + for this are that this project is best served as shipping with the + rest of GPGME and the documentation ought to match that. There are + also aspects of Org Mode's publishing features which are superior + to the defaults of reST, including the capacity to generate fully + validating strict XHTML output. + + If reST files are required at a later point for future inclusion + with other Python packages, then that format can be generated from + the .org files with Pandoc before being leveraged by either + Docutils, Sphinx or something else. + + While there are some advanced typesetting features of reST which + are not directly available to Org Mode, more often than not those + features are best implemented with either HTML and CSS, with LaTeX + to produce a PDF or via a number of XML solutions. Both reST and + Org Mode have multiple paths by which to achieve all of these. diff --git a/lang/python/doc/meta/old-commits.log b/lang/python/doc/meta/old-commits.log new file mode 100644 index 0000000..93661e3 --- /dev/null +++ b/lang/python/doc/meta/old-commits.log @@ -0,0 +1,2445 @@ +commit 2145348ec54c6027f2ea20f695de0277e2871405 +Merge: 348ba88 2036f1a +Author: Ben McGinnes <ben@adversary.org> +Date: Wed May 6 03:04:19 2015 +1000 + + Merge pull request #4 from Hasimir/master + + history + +commit 2036f1a0a670a0561993e195c458059220b36114 +Merge: dbabf0c 348ba88 +Author: Ben McGinnes <ben@adversary.org> +Date: Wed May 6 02:57:44 2015 +1000 + + Merge branch 'master' of github:adversary-org/pyme3 + +commit dbabf0cf1f2985755c2293b619011832e34faa9c +Author: Ben McGinnes <ben@adversary.org> +Date: Wed May 6 02:52:23 2015 +1000 + + Added a short history + + * A (very) brief summary of the project's history since 2002. + * Deals with why the commit log in the GPGME repo does not include the + history of PyME. + * Mentions that intact git repos will be maintained, but not where they + are (one will be on github, another will be in a user directory on + playfair.gnupg.org). + + docs/Short_History.rst | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 57 insertions(+) + +commit 348ba883424778c711c04ae9b66035ccdb36eb8c +Merge: 127d0a5 7c37a27 +Author: Ben McGinnes <ben@adversary.org> +Date: Wed May 6 02:21:34 2015 +1000 + + Merge pull request #3 from Hasimir/master + + Version release preparation + +commit 7c37a27a6845c58222d4d947c2efbe38e955b612 +Merge: f692cff 127d0a5 +Author: Ben McGinnes <ben@adversary.org> +Date: Wed May 6 02:17:14 2015 +1000 + + Merge branch 'master' of github:adversary-org/pyme3 + +commit f692cff50a89c2c61acdbd3d7dd60f5ce3cd15af +Author: Ben McGinnes <ben@adversary.org> +Date: Wed May 6 02:09:44 2015 +1000 + + TODO update + + * Removed reference to GitHub, replaced with impending new home at gnupg.org. + + docs/TODO.rst | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +commit bd5ccf9e3bfe69fa681613757577e87b72ca08ec +Author: Ben McGinnes <ben@adversary.org> +Date: Wed May 6 02:00:44 2015 +1000 + + Version bump + + * Bumped version number to 0.9.1 to keep it somewhat in line with the + existing PyME project, even though there will be some divergence at + some point (or even re-merging, depending on how many of the Python 3 + modifications can be back-ported to the Python 2 version). + * Updated the author and copyright information to reflect the two + current authors (Martin and I). + * Replaced Igor's contact details with mine. + * Replaced project home page with the GnuPG one. + + pyme/version.py | 16 +++++++++------- + 1 file changed, 9 insertions(+), 7 deletions(-) + +commit ec167512f4ca88d8f6e89e2ae831798c8283b4df +Author: Ben McGinnes <ben@adversary.org> +Date: Wed May 6 01:48:01 2015 +1000 + + README preparation. + + * Changes in preparation for impending move of code to the GnuPG git + server as a part of GPGME. + + README.rst | 14 +++++++------- + 1 file changed, 7 insertions(+), 7 deletions(-) + +commit 8a48515e884c36b5bdb24a13cb4d2e49f4ee6f17 +Author: Ben McGinnes <ben@adversary.org> +Date: Wed May 6 01:43:53 2015 +1000 + + TODO moved to docs + + * As it says. + + TODO.rst | 25 ------------------------- + docs/TODO.rst | 25 +++++++++++++++++++++++++ + 2 files changed, 25 insertions(+), 25 deletions(-) + +commit f968c777472f01f308f6e57eac1740bf5c76c205 +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 16:52:13 2015 +1000 + + Started another TODO file. + + TODO.rst | 25 +++++++++++++++++++++++++ + 1 file changed, 25 insertions(+) + +commit 127d0a56fa9f7ad1d4fb39d0b529b890a8d67365 +Merge: db72dea 44837f6 +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 14:59:44 2015 +1000 + + Merge pull request #2 from Hasimir/master + + Minor editing. + +commit 44837f6e50fc539c86aef1f75a6a3538b02029ea +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 14:56:55 2015 +1000 + + Minor editing. + + * Fixed another URL. + * Changed Py3 version's version number to v0.9.1-beta0. + + README.rst | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +commit db72deaae19c3513391df040bcaf66a88d9213af +Merge: db34286 48eb185 +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 14:26:11 2015 +1000 + + Merge pull request #1 from Hasimir/master + + Links + +commit 48eb1856cb0739cc9f0b9084da9d965e1fc7fddd +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 14:22:30 2015 +1000 + + Links + + * Fixed URLs for authors. + * Updated my entry to point to github location. + ** I strongly suspect the result of this work will be concurrent + projects, so preparing for that eventuality with this repo. + + README.rst | 8 ++++---- + 1 file changed, 4 insertions(+), 4 deletions(-) + +commit db3428659783f30b9a76204403daedf9fc4cf7cf +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 11:29:00 2015 +1000 + + Explicit over Implicit ... + + ... isn't just for code. + + * Removed the 2to3 working directory and its contents. + * Made the README.rst file a little more clear that this branch is for + Python 3 (set Python 3.2 as a fairly arbitrary requirement for the + moment, but will probably raise this to 3.3). + + 2to3/2to3-output-remaining.log | 60 --- + 2to3/2to3-output-setup.log | 35 -- + 2to3/2to3-output.log | 950 ----------------------------------------- + README.rst | 10 +- + 4 files changed, 7 insertions(+), 1048 deletions(-) + +commit 3edf07a4ba8a86af3a33246234d6e133074862af +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 11:19:41 2015 +1000 + + Added authors. + + * In alphabetical order. + * Mine will need updating once Martin and I have decided what to do + regarding the two main branches. + + README.rst | 12 ++++++++++++ + 1 file changed, 12 insertions(+) + +commit 811eb14b53e8856312d99f46b77215f7f9bd672c +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 10:23:00 2015 +1000 + + Docs and other things. + + * Now able to import pyme.core without error, indicates port process is + successful. + * Code is *not* compatible with the Python 2 version. + * Will need to consider making this a parallel project with the master + branch. + * Got rid of the .org TODO file. + * Changed the README to use the reST file extension since it's full of + reST anyway. + + 2to3/TODO.org | 5 ----- + README.rst | 32 ++++++++++++++++++++++++++++++++ + README.txt | 32 -------------------------------- + 3 files changed, 32 insertions(+), 37 deletions(-) + +commit 79e784bdcce1de6f7856921b5431044c62c6f015 +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 10:18:40 2015 +1000 + + Fixed another implicit import by making it explicit. Hopefully this is the last one. + + pyme/util.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit 2b52b46ccda3e7abcc50eed0745062259d698661 +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 10:16:01 2015 +1000 + + Fixed another implicit import by making it explicit. + + pyme/errors.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit 409c8fd565e21f23cd41daaeffc867e6d23a0863 +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 10:08:22 2015 +1000 + + Bytes vs. Unicode + + * Trying PyBytes instead of PyUnicode. + + gpgme.i | 14 +++++++------- + helpers.c | 8 ++++---- + 2 files changed, 11 insertions(+), 11 deletions(-) + +commit d8164aa2ae98bf8c807c16e2d9be12c5fbea7cfd +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 09:22:58 2015 +1000 + + String to Unicode + + * Replaced all instances of PyString with PyUnicode (and hoping there's + no byte data in there). + + gpgme.i | 14 +++++++------- + helpers.c | 8 ++++---- + 2 files changed, 11 insertions(+), 11 deletions(-) + +commit bd99b7865656e559b17c419c6b64b412a22c6c44 +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 09:17:06 2015 +1000 + + PyInt_AsLong + + * Replaced all instances of PyInt with PyLong, as per C API docs. + + gpgme.i | 4 ++-- + helpers.c | 8 ++++---- + 2 files changed, 6 insertions(+), 6 deletions(-) + +commit 3c91e2ccf8ca788b51e3308e292c6b64888fdb15 +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 05:59:36 2015 +1000 + + Import correction + + * Once pygpgme.py is generated and moved, it will be in the right + directory for the explicit "from . import pygpgme" to be correct. + + pyme/core.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit 23a49e7070812ff1ce138d8d4cc46d0b80328897 +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 05:38:29 2015 +1000 + + The -py3 flag. + + Makefile | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit b1549587d6db5e33081b9c20f75d1348a1d25938 +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 05:01:42 2015 +1000 + + Fixed indentation - 4. + + pyme/core.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit a685142ce46761ee6f5176e90717176e38e0d24f +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 05:00:16 2015 +1000 + + Fixed indentation - 3. + + pyme/core.py | 5 ++--- + 1 file changed, 2 insertions(+), 3 deletions(-) + +commit 488a70b490cc64eb1c47d2483cb2f4079c6767f7 +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 04:53:21 2015 +1000 + + Pet Peeve + + def pet_peeve(self): + peeve = print("people who don't press return after a colon!") + + FFS! + + pyme/core.py | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) + +commit a5d38eb47d64bb17bb609fe594dae2aca480bac9 +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 04:47:54 2015 +1000 + + Fixed indentation - 2. + + pyme/core.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit 476a207f732b8559abb1ea3c23147c0e34804730 +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 04:46:01 2015 +1000 + + Fixed indentation. + + pyme/core.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +commit 0572900eba9bcd9b0283c7d8e022e8972f06f9f8 +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 04:43:49 2015 +1000 + + Replaced all tabs with 4 spaces. + + pyme/core.py | 18 +++++++++--------- + 1 file changed, 9 insertions(+), 9 deletions(-) + +commit 78c0b7677e94ce1e11b8cdb833a9064527187330 +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 04:39:07 2015 +1000 + + SWIG flags in the wrong place. + + Makefile | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +commit dfa7f2589963494a8f89277560d8c1116604a3c8 +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 04:35:09 2015 +1000 + + Fixed subprocess call for swig (again). + + setup.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit 249bfd8c714dcda53127b99b6cc8a6c7c4a99f20 +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 04:32:40 2015 +1000 + + Fixed subprocess call for swig. + + setup.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit 6fd7e719cf4c975f466ceb39835db7007df36fb2 +Author: Ben McGinnes <ben@adversary.org> +Date: Sun May 3 03:51:48 2015 +1000 + + Linking swig to py3 + + * Changed the swig invocations to run with the -python -py3 flags explicitly. + + Makefile | 4 ++-- + setup.py | 2 +- + 2 files changed, 3 insertions(+), 3 deletions(-) + +commit 7a6b584f50ed6ddc8617a642185eea1f24ff791a +Author: Ben McGinnes <ben@adversary.org> +Date: Sat May 2 11:12:00 2015 +1000 + + String fun + + * streamlined confdata details, including decoding strom binary to string. + + setup.py | 4 +--- + 1 file changed, 1 insertion(+), 3 deletions(-) + +commit f7fd3f270592021a95a8f779bfe85ac18f4e390b +Author: Ben McGinnes <ben@adversary.org> +Date: Sat May 2 10:46:59 2015 +1000 + + Open File + + * Removed deprecated file() and replaced with open(). + + examples/PyGtkGpgKeys.py | 2 +- + examples/pygpa.py | 6 +++--- + gpgme-h-clean.py | 2 +- + 3 files changed, 5 insertions(+), 5 deletions(-) + +commit 4227d486f9558015e7e548d71085e58e1b50ec08 +Author: Ben McGinnes <ben@adversary.org> +Date: Sat May 2 10:36:15 2015 +1000 + + print() fix + + * Makefile includes a python print, changed from statement to function. + + Makefile | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit 406f7f2567b701502186fe0a325dc2a3491ff7f8 +Author: Ben McGinnes <ben@adversary.org> +Date: Sat May 2 10:28:42 2015 +1000 + + Updated Makefile + + * set make to use python3 instead. + * This will mean a successful port may need to be maintained seperately + from the original python2 code instead of merged, but ought to be able + to share most things. So maybe merge with separated make files or a + pre-make script to set python2 or python3 prior to building ... decide + later, after it works. + + Makefile | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +commit 90b3efa5b193d37e08dc9b4ee766ba9ebc9412af +Author: Ben McGinnes <ben@adversary.org> +Date: Sat May 2 10:15:20 2015 +1000 + + Env and a little license issue + + * Updated all the /usr/bin/env paths to point to python3. + * Also fixed the hard coded /usr/bin/python paths. + * Updated part of setup.py which gave the impression this package was + only licensed under the GPL (it's actually licensed under the LGPL as + well, essentially the same dual licensing as the GPGME library). + + examples/PyGtkGpgKeys.py | 2 +- + examples/delkey.py | 2 +- + examples/encrypt-to-all.py | 2 +- + examples/exportimport.py | 2 +- + examples/genkey.py | 2 +- + examples/inter-edit.py | 2 +- + examples/pygpa.py | 2 +- + examples/sign.py | 2 +- + examples/signverify.py | 2 +- + examples/simple.py | 2 +- + examples/t-edit.py | 2 +- + examples/testCMSgetkey.py | 2 +- + examples/verifydetails.py | 2 +- + gpgme-h-clean.py | 2 +- + setup.py | 4 ++-- + 15 files changed, 16 insertions(+), 16 deletions(-) + +commit 1a4b55dbccd2774344352e579130bf494bc5fa4b +Author: Ben McGinnes <ben@adversary.org> +Date: Sat May 2 08:50:54 2015 +1000 + + Removed extraneous files. + + * The two .bak files. + + pyme/errors.py.bak | 46 --------------------- + setup.py.bak | 116 ----------------------------------------------------- + 2 files changed, 162 deletions(-) + +commit 208879d4f2a6d0514c3f8ee2fc0da8bba42350de +Author: Ben McGinnes <ben@adversary.org> +Date: Sat May 2 08:19:37 2015 +1000 + + Added TODO.org + + * TODO list in Emacs org-mode. + * Will eventually be removed along with this entire directory when the + porting process is complete. + + 2to3/TODO.org | 5 +++++ + 1 file changed, 5 insertions(+) + +commit 1548bf201059638675c5387c6f124d4b703363a9 +Author: Ben McGinnes <ben@adversary.org> +Date: Sat May 2 07:58:40 2015 +1000 + + 2to3 conversion of remaining files + + * Ran the extended version against all the unmodified python files. + * Only pyme/errors.py required additional work. + + 2to3/2to3-output-remaining.log | 60 ++++++++++++++++++++++++++++++++++++++++++ + pyme/errors.py | 2 +- + pyme/errors.py.bak | 46 ++++++++++++++++++++++++++++++++ + 3 files changed, 107 insertions(+), 1 deletion(-) + +commit 1230650bc6bbe4c14d1284f7877aa932f3e86eb4 +Author: Ben McGinnes <ben@adversary.org> +Date: Sat May 2 07:50:39 2015 +1000 + + 2to3 conversion of setup.py + + * Ran extended 2to3 command to produce python 3 code for setup.py. + * Effectively testing for what to run against the other originally + unmodified py2 files. + + 2to3/2to3-output-setup.log | 35 ++++++++++++++ + setup.py | 7 ++- + setup.py.bak | 116 +++++++++++++++++++++++++++++++++++++++++++++ + 3 files changed, 154 insertions(+), 4 deletions(-) + +commit edad44955f59aa879e95a369591717fb19eec6b7 +Author: Ben McGinnes <ben@adversary.org> +Date: Fri May 1 21:50:07 2015 +1000 + + Removing 2to3 generated .bak files. + + * Not really needed with a real VCS, but couldn't hurt to have them for + a couple of revisions. ;) + + examples/PyGtkGpgKeys.py.bak | 663 --------------- + examples/encrypt-to-all.py.bak | 65 -- + examples/exportimport.py.bak | 75 -- + examples/genkey.py.bak | 45 - + examples/inter-edit.py.bak | 57 -- + examples/pygpa.py.bak | 1457 -------------------------------- + examples/sign.py.bak | 31 - + examples/signverify.py.bak | 78 -- + examples/simple.py.bak | 52 -- + examples/t-edit.py.bak | 59 -- + examples/testCMSgetkey.py.bak | 45 - + examples/verifydetails.py.bak | 100 --- + gpgme-h-clean.py.bak | 42 - + pyme/callbacks.py.bak | 47 -- + pyme/constants/data/__init__.py.bak | 4 - + pyme/constants/keylist/__init__.py.bak | 4 - + pyme/constants/sig/__init__.py.bak | 4 - + pyme/core.py.bak | 463 ---------- + pyme/util.py.bak | 72 -- + pyme/version.py.bak | 41 - + 20 files changed, 3404 deletions(-) + +commit 1cfc3c969f885ed191610bffbbd60ac23fdd349e +Author: Ben McGinnes <ben@adversary.org> +Date: Fri May 1 21:45:50 2015 +1000 + + 2to3 conversion log + + * The output of the command to convert the code from Python 2 to 3. + * Note: this contains the list of files which were not modified and + which will or may need to be modified. + + 2to3/2to3-output.log | 950 +++++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 950 insertions(+) + +commit 078f6cf878aa62d12704fab424198a613a24cc8c +Author: Ben McGinnes <ben@adversary.org> +Date: Fri May 1 21:36:58 2015 +1000 + + 2to3 conversion of pyme master + + * Branch from commit 459f3eca659b4949e394c4a032d9ce2053e6c721 + * Ran this: or x in `find . | egrep .py$` ; do 2to3 -w $x; done ; + * Multiple files not modified, will record elsewhere (see next commit). + + examples/PyGtkGpgKeys.py | 10 +- + examples/PyGtkGpgKeys.py.bak | 663 +++++++++++++++ + examples/encrypt-to-all.py | 12 +- + examples/encrypt-to-all.py.bak | 65 ++ + examples/exportimport.py | 20 +- + examples/exportimport.py.bak | 75 ++ + examples/genkey.py | 2 +- + examples/genkey.py.bak | 45 + + examples/inter-edit.py | 8 +- + examples/inter-edit.py.bak | 57 ++ + examples/pygpa.py | 40 +- + examples/pygpa.py.bak | 1457 ++++++++++++++++++++++++++++++++ + examples/sign.py | 2 +- + examples/sign.py.bak | 31 + + examples/signverify.py | 18 +- + examples/signverify.py.bak | 78 ++ + examples/simple.py | 8 +- + examples/simple.py.bak | 52 ++ + examples/t-edit.py | 12 +- + examples/t-edit.py.bak | 59 ++ + examples/testCMSgetkey.py | 8 +- + examples/testCMSgetkey.py.bak | 45 + + examples/verifydetails.py | 34 +- + examples/verifydetails.py.bak | 100 +++ + gpgme-h-clean.py | 2 +- + gpgme-h-clean.py.bak | 42 + + pyme/callbacks.py | 6 +- + pyme/callbacks.py.bak | 47 ++ + pyme/constants/data/__init__.py | 2 +- + pyme/constants/data/__init__.py.bak | 4 + + pyme/constants/keylist/__init__.py | 2 +- + pyme/constants/keylist/__init__.py.bak | 4 + + pyme/constants/sig/__init__.py | 2 +- + pyme/constants/sig/__init__.py.bak | 4 + + pyme/core.py | 26 +- + pyme/core.py.bak | 463 ++++++++++ + pyme/util.py | 6 +- + pyme/util.py.bak | 72 ++ + pyme/version.py | 2 +- + pyme/version.py.bak | 41 + + 40 files changed, 3515 insertions(+), 111 deletions(-) + +commit 459f3eca659b4949e394c4a032d9ce2053e6c721 +Merge: c5966ab dae7f14 +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Wed Jul 9 10:48:33 2014 +0100 + + Merged in jerrykan/pyme/fix_setup_26 (pull request #1) + + Provide support for using setup.py with Python v2.6 + +commit dae7f14a54e6c2bde0ad4da7308cc7fc0d0c0469 +Author: John Kristensen <john.kristensen@dpipwe.tas.gov.au> +Date: Wed Jul 9 15:54:39 2014 +1000 + + Provide support for using setup.py with Python v2.6 + + The setup.py script uses subprocess.check_output() which was introduced + in Python v2.7. The equivalent functionality can be achieved without + adding much extra code and provide support for Python v2.6. + + setup.py | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +commit c5966abec9d772b3922d32650da288fd50a217be +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Thu May 15 19:43:00 2014 +0100 + + README.txt in ReST, including headlines + + README.txt | 8 ++++++-- + 1 file changed, 6 insertions(+), 2 deletions(-) + +commit 43ee8c6f34fa9b6d3975aa6ea60b3d4a741fa721 +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Thu May 15 19:37:15 2014 +0100 + + README.txt in ReST + + README.txt | 25 +++++++++++++------------ + 1 file changed, 13 insertions(+), 12 deletions(-) + +commit f71a369484cba8801df23ccc5842335fa496c0df +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Thu May 15 19:28:12 2014 +0100 + + added MANIFEST.in and README.txt (instead of .md) + + MANIFEST.in | 6 ++++++ + README.md | 27 --------------------------- + README.txt | 27 +++++++++++++++++++++++++++ + 3 files changed, 33 insertions(+), 27 deletions(-) + +commit d0d6755229f920b0bed043e9c2731de2d57c096c +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Tue May 13 09:52:44 2014 +0100 + + added mailing list to README + + README.md | 19 ++++++++++++++++--- + 1 file changed, 16 insertions(+), 3 deletions(-) + +commit 30ca60ddf92df684de261cb24c83c68089be0adc +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Sun May 11 13:34:28 2014 +0100 + + we don't need a separate out of date ChangeLog file + + ChangeLog | 802 -------------------------------------------------------------- + 1 file changed, 802 deletions(-) + +commit 8263f1a6d38fdb7f5f3dd5c7e28f83caa7528a08 +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Sun May 11 13:32:31 2014 +0100 + + adding README.md + + README.md | 14 ++++++++++++++ + 1 file changed, 14 insertions(+) + +commit 3fc71b47e9e14b0b984801c28d722723baa4b406 +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Sat May 10 15:43:06 2014 +0100 + + ValueError -> RuntimeError + + setup.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +commit eec432abea56296b9fa36aac0d10926a2335b739 +Merge: eea6537 d2738b3 +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Sat May 10 15:41:02 2014 +0100 + + Merge branch 'master' of bitbucket.org:malb/pyme + + Conflicts: + setup.py + +commit eea6537921061b4dcfc54e00a99d3fa110e71433 +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Sat May 10 15:39:51 2014 +0100 + + check for swig + + setup.py | 8 ++++++++ + 1 file changed, 8 insertions(+) + +commit 53867bf9715ee1b4ea873bf5e2fbb7d9740a2b4a +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Sat May 10 15:35:04 2014 +0100 + + more friendly error message if gpgme is missing + + setup.py | 8 +++++++- + 1 file changed, 7 insertions(+), 1 deletion(-) + +commit d2738b35d63b1492d69641c5466103685f2d3a30 +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Sat May 10 15:35:04 2014 +0100 + + more friendly error message if gpgme is missing + + setup.py | 8 +++++++- + 1 file changed, 7 insertions(+), 1 deletion(-) + +commit c0b01240becf8ba6cf1d4c1f64b2cb4c056f5163 +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Fri May 9 15:20:24 2014 +0100 + + version number should have three digits + + pyme/version.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit 6672bb60b9bec60d38e854016c48658b57774578 +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Wed May 7 15:11:08 2014 +0100 + + bump version number for upcoming release + + pyme/version.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit 7bd6de700f33ca5d1f27bc16ebbd401f21d2e788 +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Sat May 3 19:36:25 2014 +0100 + + bump version number to indicate changes + + pyme/version.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit 4fb6bd9b3f47c1a343242ac83b326cacd12a136e +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Sat May 3 19:34:07 2014 +0100 + + pyme instead of pygpgme + + setup.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit 9548973138d78241a45ccb82333b25f2cf36ce7d +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Sat May 3 19:31:10 2014 +0100 + + dirty hack to make 'python setup.py install' work + + setup.py | 7 ++++--- + 1 file changed, 4 insertions(+), 3 deletions(-) + +commit a961d7eab9db478b7e603324bc5d243bd3c84bad +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Sat May 3 19:05:44 2014 +0100 + + moved everything down to the toplevel directory + + COPYING | 340 ++ + COPYING.LESSER | 510 +++ + ChangeLog | 802 +++++ + INSTALL | 15 + + Makefile | 104 + + debian/README.Debian | 6 + + debian/changelog | 93 + + debian/control | 34 + + debian/copyright | 25 + + debian/docs | 2 + + debian/examples | 2 + + debian/rules | 99 + + examples/PyGtkGpgKeys.glade | 1394 ++++++++ + examples/PyGtkGpgKeys.gladep | 8 + + examples/PyGtkGpgKeys.py | 663 ++++ + examples/delkey.py | 34 + + examples/encrypt-to-all.py | 65 + + examples/exportimport.py | 75 + + examples/genkey.py | 45 + + examples/inter-edit.py | 57 + + examples/pygpa.glade | 5546 +++++++++++++++++++++++++++++++ + examples/pygpa.py | 1457 ++++++++ + examples/sign.py | 31 + + examples/signverify.py | 78 + + examples/simple.py | 52 + + examples/t-edit.py | 59 + + examples/testCMSgetkey.py | 45 + + examples/verifydetails.py | 100 + + gpgme-h-clean.py | 42 + + gpgme.i | 267 ++ + helpers.c | 154 + + helpers.h | 36 + + pyme/COPYING | 340 -- + pyme/COPYING.LESSER | 510 --- + pyme/ChangeLog | 802 ----- + pyme/INSTALL | 15 - + pyme/Makefile | 104 - + pyme/__init__.py | 137 + + pyme/callbacks.py | 47 + + pyme/constants/__init__.py | 7 + + pyme/constants/data/__init__.py | 4 + + pyme/constants/data/encoding.py | 20 + + pyme/constants/event.py | 20 + + pyme/constants/import.py | 20 + + pyme/constants/keylist/__init__.py | 4 + + pyme/constants/keylist/mode.py | 20 + + pyme/constants/md.py | 20 + + pyme/constants/pk.py | 20 + + pyme/constants/protocol.py | 20 + + pyme/constants/sig/__init__.py | 4 + + pyme/constants/sig/mode.py | 20 + + pyme/constants/sigsum.py | 20 + + pyme/constants/status.py | 20 + + pyme/constants/validity.py | 20 + + pyme/core.py | 463 +++ + pyme/debian/README.Debian | 6 - + pyme/debian/changelog | 93 - + pyme/debian/control | 34 - + pyme/debian/copyright | 25 - + pyme/debian/docs | 2 - + pyme/debian/examples | 2 - + pyme/debian/rules | 99 - + pyme/errors.py | 46 + + pyme/examples/PyGtkGpgKeys.glade | 1394 -------- + pyme/examples/PyGtkGpgKeys.gladep | 8 - + pyme/examples/PyGtkGpgKeys.py | 663 ---- + pyme/examples/delkey.py | 34 - + pyme/examples/encrypt-to-all.py | 65 - + pyme/examples/exportimport.py | 75 - + pyme/examples/genkey.py | 45 - + pyme/examples/inter-edit.py | 57 - + pyme/examples/pygpa.glade | 5546 ------------------------------- + pyme/examples/pygpa.py | 1457 -------- + pyme/examples/sign.py | 31 - + pyme/examples/signverify.py | 78 - + pyme/examples/simple.py | 52 - + pyme/examples/t-edit.py | 59 - + pyme/examples/testCMSgetkey.py | 45 - + pyme/examples/verifydetails.py | 100 - + pyme/gpgme-h-clean.py | 42 - + pyme/gpgme.i | 267 -- + pyme/helpers.c | 154 - + pyme/helpers.h | 36 - + pyme/pyme/__init__.py | 137 - + pyme/pyme/callbacks.py | 47 - + pyme/pyme/constants/__init__.py | 7 - + pyme/pyme/constants/data/__init__.py | 4 - + pyme/pyme/constants/data/encoding.py | 20 - + pyme/pyme/constants/event.py | 20 - + pyme/pyme/constants/import.py | 20 - + pyme/pyme/constants/keylist/__init__.py | 4 - + pyme/pyme/constants/keylist/mode.py | 20 - + pyme/pyme/constants/md.py | 20 - + pyme/pyme/constants/pk.py | 20 - + pyme/pyme/constants/protocol.py | 20 - + pyme/pyme/constants/sig/__init__.py | 4 - + pyme/pyme/constants/sig/mode.py | 20 - + pyme/pyme/constants/sigsum.py | 20 - + pyme/pyme/constants/status.py | 20 - + pyme/pyme/constants/validity.py | 20 - + pyme/pyme/core.py | 463 --- + pyme/pyme/errors.py | 46 - + pyme/pyme/util.py | 72 - + pyme/pyme/version.py | 41 - + pyme/setup.py | 99 - + pyme/util.py | 72 + + pyme/version.py | 41 + + setup.py | 99 + + 108 files changed, 13384 insertions(+), 13384 deletions(-) + +commit 8148cdd424c434e833ce427612ea8c89abc6e41c +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Sat May 3 18:58:52 2014 +0100 + + removing pyme-web + + pyme-web/Makefile | 15 - + pyme-web/default.css | 37 -- + pyme-web/doc/gpgme/ASCII-Armor.html | 57 --- + pyme-web/doc/gpgme/Advanced-Key-Editing.html | 98 ---- + pyme-web/doc/gpgme/Algorithms.html | 47 -- + pyme-web/doc/gpgme/Building-the-Source.html | 82 ---- + .../doc/gpgme/Callback-Based-Data-Buffers.html | 148 ------ + pyme-web/doc/gpgme/Cancellation.html | 67 --- + pyme-web/doc/gpgme/Concept-Index.html | 186 ------- + pyme-web/doc/gpgme/Context-Attributes.html | 52 -- + pyme-web/doc/gpgme/Contexts.html | 61 --- + pyme-web/doc/gpgme/Creating-Contexts.html | 49 -- + pyme-web/doc/gpgme/Creating-Data-Buffers.html | 47 -- + pyme-web/doc/gpgme/Creating-a-Signature.html | 143 ------ + pyme-web/doc/gpgme/Crypto-Engine.html | 79 --- + pyme-web/doc/gpgme/Crypto-Operations.html | 67 --- + .../doc/gpgme/Cryptographic-Message-Syntax.html | 42 -- + .../doc/gpgme/Data-Buffer-I_002fO-Operations.html | 104 ---- + pyme-web/doc/gpgme/Data-Buffer-Meta_002dData.html | 100 ---- + pyme-web/doc/gpgme/Decrypt-and-Verify.html | 79 --- + pyme-web/doc/gpgme/Decrypt.html | 123 ----- + pyme-web/doc/gpgme/Deleting-Keys.html | 67 --- + pyme-web/doc/gpgme/Destroying-Contexts.html | 46 -- + pyme-web/doc/gpgme/Destroying-Data-Buffers.html | 70 --- + pyme-web/doc/gpgme/Encrypt.html | 45 -- + pyme-web/doc/gpgme/Encrypting-a-Plaintext.html | 147 ------ + pyme-web/doc/gpgme/Engine-Configuration.html | 65 --- + pyme-web/doc/gpgme/Engine-Information.html | 119 ----- + pyme-web/doc/gpgme/Engine-Version-Check.html | 48 -- + pyme-web/doc/gpgme/Error-Codes.html | 133 ----- + pyme-web/doc/gpgme/Error-Handling.html | 72 --- + pyme-web/doc/gpgme/Error-Sources.html | 89 ---- + pyme-web/doc/gpgme/Error-Strings.html | 80 --- + pyme-web/doc/gpgme/Error-Values.html | 159 ------ + pyme-web/doc/gpgme/Exchanging-Data.html | 58 --- + pyme-web/doc/gpgme/Exporting-Keys.html | 101 ---- + pyme-web/doc/gpgme/Features.html | 59 --- + pyme-web/doc/gpgme/File-Based-Data-Buffers.html | 74 --- + pyme-web/doc/gpgme/Function-and-Data-Index.html | 229 --------- + pyme-web/doc/gpgme/Generating-Keys.html | 144 ------ + pyme-web/doc/gpgme/Getting-Started.html | 55 --- + pyme-web/doc/gpgme/Hash-Algorithms.html | 59 --- + pyme-web/doc/gpgme/Header.html | 53 -- + .../doc/gpgme/I_002fO-Callback-Example-GDK.html | 85 ---- + .../gpgme/I_002fO-Callback-Example-GTK_002b.html | 86 ---- + .../doc/gpgme/I_002fO-Callback-Example-Qt.html | 99 ---- + pyme-web/doc/gpgme/I_002fO-Callback-Example.html | 259 ---------- + pyme-web/doc/gpgme/I_002fO-Callback-Interface.html | 142 ------ + pyme-web/doc/gpgme/Importing-Keys.html | 171 ------- + pyme-web/doc/gpgme/Included-Certificates.html | 70 --- + pyme-web/doc/gpgme/Information-About-Keys.html | 207 -------- + .../doc/gpgme/Information-About-Trust-Items.html | 75 --- + pyme-web/doc/gpgme/Introduction.html | 53 -- + pyme-web/doc/gpgme/Key-Listing-Mode.html | 99 ---- + pyme-web/doc/gpgme/Key-Management.html | 260 ---------- + pyme-web/doc/gpgme/Key-Signatures.html | 130 ----- + .../doc/gpgme/Largefile-Support-_0028LFS_0029.html | 110 ----- + pyme-web/doc/gpgme/Library-Copying.html | 542 --------------------- + pyme-web/doc/gpgme/Library-Version-Check.html | 97 ---- + pyme-web/doc/gpgme/Listing-Keys.html | 204 -------- + pyme-web/doc/gpgme/Listing-Trust-Items.html | 88 ---- + pyme-web/doc/gpgme/Locale.html | 69 --- + pyme-web/doc/gpgme/Manipulating-Data-Buffers.html | 45 -- + pyme-web/doc/gpgme/Manipulating-Keys.html | 63 --- + pyme-web/doc/gpgme/Manipulating-Trust-Items.html | 62 --- + pyme-web/doc/gpgme/Memory-Based-Data-Buffers.html | 107 ---- + pyme-web/doc/gpgme/Multi-Threading.html | 93 ---- + pyme-web/doc/gpgme/OpenPGP.html | 44 -- + pyme-web/doc/gpgme/Overview.html | 57 --- + pyme-web/doc/gpgme/Passphrase-Callback.html | 101 ---- + pyme-web/doc/gpgme/Preparation.html | 54 -- + pyme-web/doc/gpgme/Progress-Meter-Callback.html | 80 --- + pyme-web/doc/gpgme/Protocol-Selection.html | 60 --- + pyme-web/doc/gpgme/Protocols-and-Engines.html | 82 ---- + pyme-web/doc/gpgme/Public-Key-Algorithms.html | 74 --- + .../doc/gpgme/Registering-I_002fO-Callbacks.html | 81 --- + pyme-web/doc/gpgme/Run-Control.html | 53 -- + pyme-web/doc/gpgme/Selecting-Signers.html | 64 --- + pyme-web/doc/gpgme/Sign.html | 50 -- + pyme-web/doc/gpgme/Signal-Handling.html | 61 --- + pyme-web/doc/gpgme/Signature-Notation-Data.html | 85 ---- + pyme-web/doc/gpgme/Text-Mode.html | 63 --- + pyme-web/doc/gpgme/Trust-Item-Management.html | 68 --- + pyme-web/doc/gpgme/Using-Automake.html | 74 --- + pyme-web/doc/gpgme/Using-External-Event-Loops.html | 74 --- + pyme-web/doc/gpgme/Using-Libtool.html | 44 -- + pyme-web/doc/gpgme/Verify.html | 492 ------------------- + pyme-web/doc/gpgme/Waiting-For-Completion.html | 77 --- + pyme-web/doc/gpgme/index.html | 169 ------- + pyme-web/doc/pyme/index.html | 164 ------- + pyme-web/doc/pyme/pyme.callbacks.html | 42 -- + .../doc/pyme/pyme.constants.data.encoding.html | 48 -- + pyme-web/doc/pyme/pyme.constants.data.html | 29 -- + pyme-web/doc/pyme/pyme.constants.event.html | 48 -- + pyme-web/doc/pyme/pyme.constants.html | 39 -- + pyme-web/doc/pyme/pyme.constants.import.html | 49 -- + pyme-web/doc/pyme/pyme.constants.keylist.html | 29 -- + pyme-web/doc/pyme/pyme.constants.keylist.mode.html | 49 -- + pyme-web/doc/pyme/pyme.constants.md.html | 58 --- + pyme-web/doc/pyme/pyme.constants.pk.html | 50 -- + pyme-web/doc/pyme/pyme.constants.protocol.html | 48 -- + pyme-web/doc/pyme/pyme.constants.sig.html | 29 -- + pyme-web/doc/pyme/pyme.constants.sig.mode.html | 47 -- + pyme-web/doc/pyme/pyme.constants.sigsum.html | 55 --- + pyme-web/doc/pyme/pyme.constants.status.html | 126 ----- + pyme-web/doc/pyme/pyme.constants.validity.html | 50 -- + pyme-web/doc/pyme/pyme.core.html | 277 ----------- + pyme-web/doc/pyme/pyme.errors.html | 82 ---- + pyme-web/doc/pyme/pyme.html | 164 ------- + pyme-web/doc/pyme/pyme.util.html | 81 --- + pyme-web/doc/pyme/pyme.version.html | 37 -- + pyme-web/index.html | 72 --- + 112 files changed, 10551 deletions(-) + +commit 684d95feb7e10e538a56fb1b27f1456111bacb60 +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Mon Jan 6 17:44:20 2014 +0100 + + fixing op_export_keys() + + the conversion of gpgme_key_t [] was restricted to gpgme_key_t [] with the + name recv, i.e. only the use-cases of encryption were covered. + + see: http://sourceforge.net/mailarchive/forum.php?forum_name=pyme-help&max_rows=25&style=nested&viewmonth=201309 + + pyme/gpgme.i | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +commit 658d23b95110d21eeb50abf4e74701a667521a88 +Author: Martin Albrecht <martinralbrecht@googlemail.com> +Date: Mon Jan 6 17:41:33 2014 +0100 + + deleting CVSROOT + + CVSROOT/checkoutlist | 13 ------------- + CVSROOT/commitinfo | 15 --------------- + CVSROOT/config | 21 --------------------- + CVSROOT/cvswrappers | 19 ------------------- + CVSROOT/editinfo | 21 --------------------- + CVSROOT/loginfo | 26 -------------------------- + CVSROOT/modules | 26 -------------------------- + CVSROOT/notify | 12 ------------ + CVSROOT/rcsinfo | 13 ------------- + CVSROOT/taginfo | 20 -------------------- + CVSROOT/verifymsg | 21 --------------------- + 11 files changed, 207 deletions(-) + +commit 576b555499c094c4786d42de9e59aa9826009b89 +Author: convert-repo <devnull@localhost> +Date: Mon Jan 6 15:22:44 2014 +0000 + + update tags + +commit 2dcf0c5b702eb5a18c66ff1e42a72eaa7427af1d +Author: belyi <devnull@localhost> +Date: Wed Nov 26 02:38:33 2008 +0000 + + Move Windows specific fix from helpers.c to helpers.h so that it works + for edit callback as well as for the passphrase one. + + pyme/helpers.c | 5 ----- + pyme/helpers.h | 5 +++++ + 2 files changed, 5 insertions(+), 5 deletions(-) + +commit 42a035f2ef62470fea7a7f8ee33a1297fa90a603 +Author: belyi <devnull@localhost> +Date: Mon Nov 24 21:44:30 2008 +0000 + + Update the way build directives are constructed on MinGW to have a bit + more robust. Update PyMe build version to 0.8.1 in version.py + + pyme/pyme/version.py | 2 +- + pyme/setup.py | 10 ++++++++-- + 2 files changed, 9 insertions(+), 3 deletions(-) + +commit 3aaa20fbcba17066c9ffd580f5209946022793a2 +Author: belyi <devnull@localhost> +Date: Mon Nov 24 06:57:11 2008 +0000 + + Update changelog + + pyme/debian/changelog | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +commit 689ff46b2550547e3883f809a6dc40c22c3e137e +Author: belyi <devnull@localhost> +Date: Mon Nov 24 06:50:41 2008 +0000 + + Fix hang problem on Windows when password is written to a filehandle. + Fix the way path is constructed on MinGW platform. + + pyme/helpers.c | 5 +++++ + pyme/setup.py | 4 ++-- + 2 files changed, 7 insertions(+), 2 deletions(-) + +commit 852a60d541d66cb56f40378182b976fd87a02c46 +Author: belyi <devnull@localhost> +Date: Sun Nov 23 04:31:31 2008 +0000 + + Add Bernard's example testCMSgetkey.py and his updates for + verifydetails.py + + pyme/examples/testCMSgetkey.py | 45 ++++++++++++++++++++++++++++++++++++++++++ + pyme/examples/verifydetails.py | 43 +++++++++++++++++++++++++++++----------- + 2 files changed, 77 insertions(+), 11 deletions(-) + +commit f080527d9184f3360f0a8ef6136b9a188d8e7d2a +Author: belyi <devnull@localhost> +Date: Thu May 29 18:29:37 2008 +0000 + + Remove debian packaging for python2.3 since it is removed from both + testing and unstable dists. + Update docs build target to have correct PYTHONPATH set. + + pyme/Makefile | 2 +- + pyme/debian/changelog | 4 +++- + pyme/debian/control | 4 ++-- + pyme/debian/rules | 2 -- + 4 files changed, 6 insertions(+), 6 deletions(-) + +commit c25d133fcbadf3c7f6e655586b4a05d6e3cf6f0b +Author: belyi <devnull@localhost> +Date: Thu Apr 3 13:37:12 2008 +0000 + + Forgot to adjust mainText margin. Doing it now. + + pyme-web/default.css | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit 897286a54a32336d060cd03305cdecb7905f34f1 +Author: belyi <devnull@localhost> +Date: Thu Apr 3 13:00:11 2008 +0000 + + Fix an error in default.css and make index.html "Standards Compliant". + + pyme-web/default.css | 2 +- + pyme-web/index.html | 7 ++++--- + 2 files changed, 5 insertions(+), 4 deletions(-) + +commit 4e049212bd214449cc0ba1ce06e00782783f328a +Author: belyi <devnull@localhost> +Date: Thu Apr 3 12:38:42 2008 +0000 + + Adjust spacing between links. + + pyme-web/default.css | 9 ++++++--- + 1 file changed, 6 insertions(+), 3 deletions(-) + +commit cb2bddfbd77483b1deb14f2eab0715a03dd33fcd +Author: belyi <devnull@localhost> +Date: Wed Apr 2 22:50:21 2008 +0000 + + Make style a big more IE friendly. + + pyme-web/default.css | 15 +++++++++++---- + 1 file changed, 11 insertions(+), 4 deletions(-) + +commit ad66f0a1bb01b46baac328e9fee439b35a60c232 +Author: belyi <devnull@localhost> +Date: Wed Apr 2 11:58:32 2008 +0000 + + Make GPGME documentation a bit more web friendly on the index.html page. + + pyme-web/doc/gpgme/Algorithms.html | 2 +- + pyme-web/doc/gpgme/Concept-Index.html | 2 +- + pyme-web/doc/gpgme/Contexts.html | 2 +- + pyme-web/doc/gpgme/Error-Handling.html | 2 +- + pyme-web/doc/gpgme/Exchanging-Data.html | 2 +- + pyme-web/doc/gpgme/Function-and-Data-Index.html | 2 +- + pyme-web/doc/gpgme/Introduction.html | 4 +- + pyme-web/doc/gpgme/Library-Copying.html | 2 +- + pyme-web/doc/gpgme/Preparation.html | 2 +- + pyme-web/doc/gpgme/Protocols-and-Engines.html | 2 +- + pyme-web/doc/gpgme/index.html | 229 +----------------------- + 11 files changed, 12 insertions(+), 239 deletions(-) + +commit 4f57c0ccb049d4442e7732e2d1d05dabffd2a21d +Author: belyi <devnull@localhost> +Date: Wed Apr 2 06:12:57 2008 +0000 + + Add missing core.set_locale() to set default locale for contexts. + + pyme/debian/changelog | 2 +- + pyme/pyme/core.py | 4 ++++ + 2 files changed, 5 insertions(+), 1 deletion(-) + +commit acf7ead3dea8590cf9fe86b67bb125837ad6ed4f +Author: belyi <devnull@localhost> +Date: Wed Apr 2 05:50:24 2008 +0000 + + Avoid leaks caused by keys. + Add set/get methods for engine info. + + pyme/debian/changelog | 10 ++++++++++ + pyme/pyme/core.py | 24 ++++++++++++++++++++++++ + 2 files changed, 34 insertions(+) + +commit df4a2fb518adbb6420d95ce74af212c87abff7e7 +Author: belyi <devnull@localhost> +Date: Wed Apr 2 04:04:41 2008 +0000 + + Update index.html to reflect new versions on the web. + + pyme-web/Makefile | 3 ++- + pyme-web/doc/gpgme/index.html | 4 +--- + pyme-web/index.html | 4 ++-- + 3 files changed, 5 insertions(+), 6 deletions(-) + +commit bd3ffc9bdf98d6aafde6b689c6c8215fa468612d +Author: belyi <devnull@localhost> +Date: Wed Apr 2 04:01:04 2008 +0000 + + Update PyMe documentation to match 0.8.0 version of the package. + + pyme-web/doc/pyme/index.html | 14 ++++----- + pyme-web/doc/pyme/pyme.constants.keylist.mode.html | 1 + + pyme-web/doc/pyme/pyme.constants.protocol.html | 4 ++- + pyme-web/doc/pyme/pyme.constants.status.html | 9 ++++++ + pyme-web/doc/pyme/pyme.core.html | 36 ++++++++++++++++++---- + pyme-web/doc/pyme/pyme.errors.html | 8 ++--- + pyme-web/doc/pyme/pyme.html | 14 ++++----- + pyme-web/doc/pyme/pyme.util.html | 17 ++++++++-- + pyme-web/doc/pyme/pyme.version.html | 14 ++++----- + 9 files changed, 82 insertions(+), 35 deletions(-) + +commit 6973a69a317608a0d0661590d701f4e3f3a21b32 +Author: belyi <devnull@localhost> +Date: Wed Apr 2 02:35:24 2008 +0000 + + Have a fix for Contents being put onto 'Function and Data Index' page. + + pyme-web/doc/gpgme/Concept-Index.html | 2 +- + pyme-web/doc/gpgme/Function-and-Data-Index.html | 153 +---------------------- + pyme-web/doc/gpgme/index.html | 154 +++++++++++++++++++++++- + 3 files changed, 155 insertions(+), 154 deletions(-) + +commit 086315964cbc2abad1187f306dcb9c72ac3257f3 +Author: belyi <devnull@localhost> +Date: Wed Apr 2 01:00:29 2008 +0000 + + Update GPGME documentation. It's for v1.1.6 now. + + pyme-web/doc/gpgme/ASCII-Armor.html | 57 ++ + pyme-web/doc/gpgme/Advanced-Key-Editing.html | 98 +++ + pyme-web/doc/gpgme/Algorithms.html | 47 ++ + pyme-web/doc/gpgme/Building-the-Source.html | 82 +++ + .../doc/gpgme/Callback-Based-Data-Buffers.html | 148 +++++ + pyme-web/doc/gpgme/Cancellation.html | 67 ++ + pyme-web/doc/gpgme/Concept-Index.html | 186 ++++++ + pyme-web/doc/gpgme/Context-Attributes.html | 52 ++ + pyme-web/doc/gpgme/Contexts.html | 61 ++ + pyme-web/doc/gpgme/Creating-Contexts.html | 49 ++ + pyme-web/doc/gpgme/Creating-Data-Buffers.html | 47 ++ + pyme-web/doc/gpgme/Creating-a-Signature.html | 143 +++++ + pyme-web/doc/gpgme/Crypto-Engine.html | 79 +++ + pyme-web/doc/gpgme/Crypto-Operations.html | 67 ++ + .../doc/gpgme/Cryptographic-Message-Syntax.html | 42 ++ + .../doc/gpgme/Data-Buffer-I_002fO-Operations.html | 104 ++++ + pyme-web/doc/gpgme/Data-Buffer-Meta_002dData.html | 100 +++ + pyme-web/doc/gpgme/Decrypt-and-Verify.html | 79 +++ + pyme-web/doc/gpgme/Decrypt.html | 123 ++++ + pyme-web/doc/gpgme/Deleting-Keys.html | 67 ++ + pyme-web/doc/gpgme/Destroying-Contexts.html | 46 ++ + pyme-web/doc/gpgme/Destroying-Data-Buffers.html | 70 +++ + pyme-web/doc/gpgme/Encrypt.html | 45 ++ + pyme-web/doc/gpgme/Encrypting-a-Plaintext.html | 147 +++++ + pyme-web/doc/gpgme/Engine-Configuration.html | 65 ++ + pyme-web/doc/gpgme/Engine-Information.html | 119 ++++ + pyme-web/doc/gpgme/Engine-Version-Check.html | 48 ++ + pyme-web/doc/gpgme/Error-Codes.html | 133 ++++ + pyme-web/doc/gpgme/Error-Handling.html | 72 +++ + pyme-web/doc/gpgme/Error-Sources.html | 89 +++ + pyme-web/doc/gpgme/Error-Strings.html | 80 +++ + pyme-web/doc/gpgme/Error-Values.html | 159 +++++ + pyme-web/doc/gpgme/Exchanging-Data.html | 58 ++ + pyme-web/doc/gpgme/Exporting-Keys.html | 101 +++ + pyme-web/doc/gpgme/Features.html | 59 ++ + pyme-web/doc/gpgme/File-Based-Data-Buffers.html | 74 +++ + pyme-web/doc/gpgme/Function-and-Data-Index.html | 380 ++++++++++++ + pyme-web/doc/gpgme/Generating-Keys.html | 144 +++++ + pyme-web/doc/gpgme/Getting-Started.html | 55 ++ + pyme-web/doc/gpgme/Hash-Algorithms.html | 59 ++ + pyme-web/doc/gpgme/Header.html | 53 ++ + .../doc/gpgme/I_002fO-Callback-Example-GDK.html | 85 +++ + .../gpgme/I_002fO-Callback-Example-GTK_002b.html | 86 +++ + .../doc/gpgme/I_002fO-Callback-Example-Qt.html | 99 +++ + pyme-web/doc/gpgme/I_002fO-Callback-Example.html | 259 ++++++++ + pyme-web/doc/gpgme/I_002fO-Callback-Interface.html | 142 +++++ + pyme-web/doc/gpgme/Importing-Keys.html | 171 +++++ + pyme-web/doc/gpgme/Included-Certificates.html | 70 +++ + pyme-web/doc/gpgme/Information-About-Keys.html | 207 +++++++ + .../doc/gpgme/Information-About-Trust-Items.html | 75 +++ + pyme-web/doc/gpgme/Introduction.html | 53 ++ + pyme-web/doc/gpgme/Key-Listing-Mode.html | 99 +++ + pyme-web/doc/gpgme/Key-Management.html | 260 ++++++++ + pyme-web/doc/gpgme/Key-Signatures.html | 130 ++++ + .../doc/gpgme/Largefile-Support-_0028LFS_0029.html | 110 ++++ + pyme-web/doc/gpgme/Library-Copying.html | 542 ++++++++++++++++ + pyme-web/doc/gpgme/Library-Version-Check.html | 97 +++ + pyme-web/doc/gpgme/Listing-Keys.html | 204 ++++++ + pyme-web/doc/gpgme/Listing-Trust-Items.html | 88 +++ + pyme-web/doc/gpgme/Locale.html | 69 +++ + pyme-web/doc/gpgme/Manipulating-Data-Buffers.html | 45 ++ + pyme-web/doc/gpgme/Manipulating-Keys.html | 63 ++ + pyme-web/doc/gpgme/Manipulating-Trust-Items.html | 62 ++ + pyme-web/doc/gpgme/Memory-Based-Data-Buffers.html | 107 ++++ + pyme-web/doc/gpgme/Multi-Threading.html | 93 +++ + pyme-web/doc/gpgme/OpenPGP.html | 44 ++ + pyme-web/doc/gpgme/Overview.html | 57 ++ + pyme-web/doc/gpgme/Passphrase-Callback.html | 101 +++ + pyme-web/doc/gpgme/Preparation.html | 54 ++ + pyme-web/doc/gpgme/Progress-Meter-Callback.html | 80 +++ + pyme-web/doc/gpgme/Protocol-Selection.html | 60 ++ + pyme-web/doc/gpgme/Protocols-and-Engines.html | 82 +++ + pyme-web/doc/gpgme/Public-Key-Algorithms.html | 74 +++ + .../doc/gpgme/Registering-I_002fO-Callbacks.html | 81 +++ + pyme-web/doc/gpgme/Run-Control.html | 53 ++ + pyme-web/doc/gpgme/Selecting-Signers.html | 64 ++ + pyme-web/doc/gpgme/Sign.html | 50 ++ + pyme-web/doc/gpgme/Signal-Handling.html | 61 ++ + pyme-web/doc/gpgme/Signature-Notation-Data.html | 85 +++ + pyme-web/doc/gpgme/Text-Mode.html | 63 ++ + pyme-web/doc/gpgme/Trust-Item-Management.html | 68 ++ + pyme-web/doc/gpgme/Using-Automake.html | 74 +++ + pyme-web/doc/gpgme/Using-External-Event-Loops.html | 74 +++ + pyme-web/doc/gpgme/Using-Libtool.html | 44 ++ + pyme-web/doc/gpgme/Verify.html | 492 +++++++++++++++ + pyme-web/doc/gpgme/Waiting-For-Completion.html | 77 +++ + pyme-web/doc/gpgme/gpgme.html | 251 -------- + pyme-web/doc/gpgme/gpgme_1.html | 76 --- + pyme-web/doc/gpgme/gpgme_10.html | 61 -- + pyme-web/doc/gpgme/gpgme_11.html | 130 ---- + pyme-web/doc/gpgme/gpgme_12.html | 82 --- + pyme-web/doc/gpgme/gpgme_13.html | 130 ---- + pyme-web/doc/gpgme/gpgme_14.html | 108 ---- + pyme-web/doc/gpgme/gpgme_15.html | 69 --- + pyme-web/doc/gpgme/gpgme_16.html | 169 ----- + pyme-web/doc/gpgme/gpgme_17.html | 63 -- + pyme-web/doc/gpgme/gpgme_18.html | 63 -- + pyme-web/doc/gpgme/gpgme_19.html | 66 -- + pyme-web/doc/gpgme/gpgme_2.html | 79 --- + pyme-web/doc/gpgme/gpgme_20.html | 120 ---- + pyme-web/doc/gpgme/gpgme_21.html | 102 --- + pyme-web/doc/gpgme/gpgme_22.html | 108 ---- + pyme-web/doc/gpgme/gpgme_23.html | 237 ------- + pyme-web/doc/gpgme/gpgme_24.html | 154 ----- + pyme-web/doc/gpgme/gpgme_25.html | 248 -------- + pyme-web/doc/gpgme/gpgme_26.html | 107 ---- + pyme-web/doc/gpgme/gpgme_27.html | 80 --- + pyme-web/doc/gpgme/gpgme_28.html | 67 -- + pyme-web/doc/gpgme/gpgme_29.html | 164 ----- + pyme-web/doc/gpgme/gpgme_3.html | 86 --- + pyme-web/doc/gpgme/gpgme_30.html | 106 ---- + pyme-web/doc/gpgme/gpgme_31.html | 232 ------- + pyme-web/doc/gpgme/gpgme_32.html | 85 --- + pyme-web/doc/gpgme/gpgme_33.html | 223 ------- + pyme-web/doc/gpgme/gpgme_34.html | 83 --- + pyme-web/doc/gpgme/gpgme_35.html | 70 --- + pyme-web/doc/gpgme/gpgme_36.html | 63 -- + pyme-web/doc/gpgme/gpgme_37.html | 66 -- + pyme-web/doc/gpgme/gpgme_38.html | 86 --- + pyme-web/doc/gpgme/gpgme_39.html | 79 --- + pyme-web/doc/gpgme/gpgme_4.html | 83 --- + pyme-web/doc/gpgme/gpgme_40.html | 89 --- + pyme-web/doc/gpgme/gpgme_41.html | 99 --- + pyme-web/doc/gpgme/gpgme_42.html | 144 ----- + pyme-web/doc/gpgme/gpgme_43.html | 152 ----- + pyme-web/doc/gpgme/gpgme_44.html | 112 ---- + pyme-web/doc/gpgme/gpgme_45.html | 101 --- + pyme-web/doc/gpgme/gpgme_46.html | 459 -------------- + pyme-web/doc/gpgme/gpgme_47.html | 292 --------- + pyme-web/doc/gpgme/gpgme_48.html | 363 ----------- + pyme-web/doc/gpgme/gpgme_49.html | 209 ------- + pyme-web/doc/gpgme/gpgme_5.html | 74 --- + pyme-web/doc/gpgme/gpgme_50.html | 88 --- + pyme-web/doc/gpgme/gpgme_51.html | 208 ------- + pyme-web/doc/gpgme/gpgme_52.html | 154 ----- + pyme-web/doc/gpgme/gpgme_53.html | 291 --------- + pyme-web/doc/gpgme/gpgme_54.html | 91 --- + pyme-web/doc/gpgme/gpgme_55.html | 107 ---- + pyme-web/doc/gpgme/gpgme_56.html | 140 ----- + pyme-web/doc/gpgme/gpgme_57.html | 106 ---- + pyme-web/doc/gpgme/gpgme_58.html | 89 --- + pyme-web/doc/gpgme/gpgme_59.html | 97 --- + pyme-web/doc/gpgme/gpgme_6.html | 77 --- + pyme-web/doc/gpgme/gpgme_60.html | 142 ----- + pyme-web/doc/gpgme/gpgme_61.html | 626 ------------------- + pyme-web/doc/gpgme/gpgme_62.html | 107 ---- + pyme-web/doc/gpgme/gpgme_63.html | 67 -- + pyme-web/doc/gpgme/gpgme_64.html | 95 --- + pyme-web/doc/gpgme/gpgme_65.html | 233 ------- + pyme-web/doc/gpgme/gpgme_66.html | 65 -- + pyme-web/doc/gpgme/gpgme_67.html | 220 ------- + pyme-web/doc/gpgme/gpgme_68.html | 75 --- + pyme-web/doc/gpgme/gpgme_69.html | 119 ---- + pyme-web/doc/gpgme/gpgme_7.html | 123 ---- + pyme-web/doc/gpgme/gpgme_70.html | 107 ---- + pyme-web/doc/gpgme/gpgme_71.html | 218 ------- + pyme-web/doc/gpgme/gpgme_72.html | 134 ---- + pyme-web/doc/gpgme/gpgme_73.html | 299 --------- + pyme-web/doc/gpgme/gpgme_74.html | 103 ---- + pyme-web/doc/gpgme/gpgme_75.html | 104 ---- + pyme-web/doc/gpgme/gpgme_76.html | 118 ---- + pyme-web/doc/gpgme/gpgme_77.html | 95 --- + pyme-web/doc/gpgme/gpgme_78.html | 71 --- + pyme-web/doc/gpgme/gpgme_79.html | 686 --------------------- + pyme-web/doc/gpgme/gpgme_8.html | 155 ----- + pyme-web/doc/gpgme/gpgme_80.html | 120 ---- + pyme-web/doc/gpgme/gpgme_81.html | 278 --------- + pyme-web/doc/gpgme/gpgme_82.html | 272 -------- + pyme-web/doc/gpgme/gpgme_83.html | 180 ------ + pyme-web/doc/gpgme/gpgme_84.html | 99 --- + pyme-web/doc/gpgme/gpgme_9.html | 104 ---- + pyme-web/doc/gpgme/gpgme_abt.html | 206 ------- + pyme-web/doc/gpgme/gpgme_fot.html | 53 -- + pyme-web/doc/gpgme/gpgme_ovr.html | 68 -- + pyme-web/doc/gpgme/gpgme_toc.html | 247 -------- + pyme-web/doc/gpgme/index.html | 497 ++++++++------- + 176 files changed, 9054 insertions(+), 13378 deletions(-) + +commit 163c1053dc761682f5a4231da163bdd0ff7162d7 +Author: belyi <devnull@localhost> +Date: Tue Apr 1 21:14:29 2008 +0000 + + Update Home page to be a bit more visitor friendly. + + pyme-web/Makefile | 2 +- + pyme-web/default.css | 27 ++++++++++++++++++++ + pyme-web/index.html | 70 +++++++++++++++++++++++++++++++++++----------------- + 3 files changed, 75 insertions(+), 24 deletions(-) + +commit 05db2d17d8fda0ab8c948bbdc0643dfc1466830d +Author: belyi <devnull@localhost> +Date: Sun Mar 30 21:27:38 2008 +0000 + + Add a rule to build binary distribution for Windows. + + pyme/Makefile | 16 ++++++++++++++-- + 1 file changed, 14 insertions(+), 2 deletions(-) + +commit 57acb1089f5f8c24323ee62fc0a7f492a496b9c0 +Author: belyi <devnull@localhost> +Date: Sat Mar 29 22:50:11 2008 +0000 + + Switch to using central location for python files (pycentral) + Update docs rule to fix location of the python source files. + + pyme/Makefile | 5 +++- + pyme/debian/changelog | 4 ++- + pyme/debian/control | 74 +++++------------------------------------------ + pyme/debian/dirs | 2 -- + pyme/debian/docs | 1 + + pyme/debian/postinst.ex | 48 ------------------------------ + pyme/debian/postrm.ex | 38 ------------------------ + pyme/debian/preinst.ex | 44 ---------------------------- + pyme/debian/prerm.ex | 39 ------------------------- + pyme/debian/rules | 50 ++++++-------------------------- + pyme/debian/setup.cfg-2.2 | 8 ----- + pyme/debian/setup.cfg-2.3 | 8 ----- + pyme/debian/setup.cfg-2.4 | 8 ----- + pyme/gpgme-h-clean.py | 2 +- + pyme/pyme/core.py | 2 +- + pyme/pyme/util.py | 2 +- + 16 files changed, 28 insertions(+), 307 deletions(-) + +commit 2b56fd10517cfbcffaa4ba98d8ea42f40f0d38a9 +Author: belyi <devnull@localhost> +Date: Sun Mar 23 02:01:12 2008 +0000 + + Turn SWIG's autodoc feature on. Ignore 'next' in the types which are lists now. + Use new style for class declarations. Specify None as a default value for + core.check_version() method. Update version.py for 0.8.0 version. + + pyme/examples/pygpa.py | 2 +- + pyme/gpgme.i | 5 +++++ + pyme/pyme/core.py | 2 +- + pyme/pyme/util.py | 5 +++-- + pyme/pyme/version.py | 6 +++--- + 5 files changed, 13 insertions(+), 7 deletions(-) + +commit df5e25d7ee4dc0aa0d429f9d009322dd8ac33bb8 +Author: belyi <devnull@localhost> +Date: Thu Mar 20 19:07:00 2008 +0000 + + Improve matching for DEPRECATED typedefs + + pyme/gpgme-h-clean.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +commit 78d8fc732848ac267ec65e9069265cd500587cdf +Author: belyi <devnull@localhost> +Date: Wed Mar 19 19:28:40 2008 +0000 + + Update API to use list when types containing 'next' field are return. + Update examples accordingly + Add verifydetails.py example + Start adding bullets for 0.8.0 version. + + pyme/Makefile | 2 +- + pyme/debian/changelog | 14 +++++++- + pyme/examples/PyGtkGpgKeys.py | 53 +++++++++++++-------------- + pyme/examples/delkey.py | 7 ++-- + pyme/examples/encrypt-to-all.py | 7 ++-- + pyme/examples/exportimport.py | 7 ++-- + pyme/examples/pygpa.py | 70 ++++++++++++++++-------------------- + pyme/examples/signverify.py | 11 +++--- + pyme/examples/verifydetails.py | 79 +++++++++++++++++++++++++++++++++++++++++ + pyme/gpgme.i | 19 +++++++++- + 10 files changed, 180 insertions(+), 89 deletions(-) + +commit 342d85b07475e7360bcd62804bf5facda039494f +Author: belyi <devnull@localhost> +Date: Mon Mar 10 01:14:16 2008 +0000 + + Change references to source files so that they point to the WebCVS browse + location. + + pyme-web/doc/pyme/index.html | 2 +- + pyme-web/doc/pyme/pyme.callbacks.html | 2 +- + pyme-web/doc/pyme/pyme.constants.data.encoding.html | 2 +- + pyme-web/doc/pyme/pyme.constants.data.html | 2 +- + pyme-web/doc/pyme/pyme.constants.event.html | 2 +- + pyme-web/doc/pyme/pyme.constants.html | 2 +- + pyme-web/doc/pyme/pyme.constants.import.html | 2 +- + pyme-web/doc/pyme/pyme.constants.keylist.html | 2 +- + pyme-web/doc/pyme/pyme.constants.keylist.mode.html | 2 +- + pyme-web/doc/pyme/pyme.constants.md.html | 2 +- + pyme-web/doc/pyme/pyme.constants.pk.html | 2 +- + pyme-web/doc/pyme/pyme.constants.protocol.html | 2 +- + pyme-web/doc/pyme/pyme.constants.sig.html | 2 +- + pyme-web/doc/pyme/pyme.constants.sig.mode.html | 2 +- + pyme-web/doc/pyme/pyme.constants.sigsum.html | 2 +- + pyme-web/doc/pyme/pyme.constants.status.html | 2 +- + pyme-web/doc/pyme/pyme.constants.validity.html | 2 +- + pyme-web/doc/pyme/pyme.core.html | 2 +- + pyme-web/doc/pyme/pyme.errors.html | 2 +- + pyme-web/doc/pyme/pyme.html | 2 +- + pyme-web/doc/pyme/pyme.util.html | 2 +- + pyme-web/doc/pyme/pyme.version.html | 2 +- + 22 files changed, 22 insertions(+), 22 deletions(-) + +commit 4139dd1d066c1a6c892d84fe45dc3e6c4aa1b803 +Author: belyi <devnull@localhost> +Date: Sat Mar 8 18:21:08 2008 +0000 + + Add core.check_version(None) to all examples since this function is used by + Gpgme to do internal initialization. Update debian/rules to use dh_pysupport + instead of deprecated dh_python. + + pyme/debian/rules | 8 +++----- + pyme/examples/PyGtkGpgKeys.py | 7 ++++++- + pyme/examples/delkey.py | 2 ++ + pyme/examples/encrypt-to-all.py | 3 +++ + pyme/examples/exportimport.py | 2 ++ + pyme/examples/genkey.py | 1 + + pyme/examples/inter-edit.py | 3 +++ + pyme/examples/pygpa.py | 5 +++++ + pyme/examples/sign.py | 2 ++ + pyme/examples/signverify.py | 2 ++ + pyme/examples/simple.py | 2 ++ + pyme/examples/t-edit.py | 3 +++ + 12 files changed, 34 insertions(+), 6 deletions(-) + +commit ae76c6176457dd38e0634cbc17d794294a3a81d2 +Author: belyi <devnull@localhost> +Date: Wed Apr 12 22:20:38 2006 +0000 + + Change name of internal package name from 'gpgme' to 'pygpgme' to avoid + conflict with gpgme.dll on Windows. + Fix build with SWIG 1.3.28. + Change version to 0.7.1 in a preparation for new release. + + pyme/Makefile | 3 +- + pyme/debian/changelog | 12 ++++ + pyme/gpgme.i | 19 +++--- + pyme/pyme/callbacks.py | 1 - + pyme/pyme/core.py | 153 +++++++++++++++++++++++++------------------------ + pyme/pyme/errors.py | 12 ++-- + pyme/pyme/util.py | 10 ++-- + pyme/pyme/version.py | 2 +- + pyme/setup.py | 4 +- + 9 files changed, 116 insertions(+), 100 deletions(-) + +commit d644383a76e9f83bc2d426628319e3c4a989dc2d +Author: belyi <devnull@localhost> +Date: Sat Dec 17 01:34:53 2005 +0000 + + Put all constants into pyme.constants package to avoid stepping on python + reserved words. + Add build rules for Mingw32 and Cygwin on Windows. Rules for Mingw under + Debian are still to come. + Fixed a small bug in pygpa.py example. + + pyme/Makefile | 11 ++++++++--- + pyme/examples/pygpa.py | 3 ++- + pyme/pyme/__init__.py | 2 +- + pyme/pyme/constants/__init__.py | 3 +++ + pyme/setup.py | 42 ++++++++++++++++++++++++++++++++++++----- + 5 files changed, 51 insertions(+), 10 deletions(-) + +commit 89eb370fcaa8adc9d219eadbaa579dde7bf06329 +Author: belyi <devnull@localhost> +Date: Mon Aug 1 03:08:32 2005 +0000 + + Imported changes provided by Joost van Baal: + Use dh_python in debian/rules and change the Section pyme belongs to from + 'libs' to 'python'. + + pyme/debian/control | 6 +++--- + pyme/debian/rules | 2 ++ + 2 files changed, 5 insertions(+), 3 deletions(-) + +commit ad76d10c2a77b45b7459c62131279e946b860891 +Author: belyi <devnull@localhost> +Date: Fri Jun 10 03:01:22 2005 +0000 + + Update 'docs' rule in Makefile to build packages first to ensure that + documentation is build for the current version of pyme and not for the + installed one. + + Added 'callbacks' into the list of visible pyme modules (__all__ var.) + + Slightly updated INSTALL file. + + pyme/INSTALL | 11 ++++++++--- + pyme/Makefile | 4 ++-- + pyme/pyme/__init__.py | 2 +- + 3 files changed, 11 insertions(+), 6 deletions(-) + +commit 2fe1a81e00721698bfa6850b3db2eb85e43d1724 +Author: belyi <devnull@localhost> +Date: Wed Jun 8 16:16:18 2005 +0000 + + Update pyme documentation to remove dead links to pyme.gpgme.html and + pyme._gpgme.html + Added reference to the installed GPGME and PyMe documentation to the head + web page. + Updated Makefile to install all *.html files and to clean *~ files in all + subdirectories + + pyme-web/Makefile | 10 ++++++---- + pyme-web/doc/pyme/index.html | 8 +++----- + pyme-web/doc/pyme/pyme.callbacks.html | 8 -------- + pyme-web/doc/pyme/pyme.core.html | 1 - + pyme-web/doc/pyme/pyme.errors.html | 8 -------- + pyme-web/doc/pyme/pyme.html | 8 +++----- + pyme-web/doc/pyme/pyme.util.html | 8 -------- + pyme-web/index.html | 9 +++++++-- + 8 files changed, 19 insertions(+), 41 deletions(-) + +commit 6aa34cce4ea0099e50b4936dfee59778157b8ca8 +Author: belyi <devnull@localhost> +Date: Wed Jun 8 15:18:20 2005 +0000 + + Added pyme and gpgme documentation. + + pyme-web/doc/gpgme/gpgme.html | 251 ++++++++ + pyme-web/doc/gpgme/gpgme_1.html | 76 +++ + pyme-web/doc/gpgme/gpgme_10.html | 61 ++ + pyme-web/doc/gpgme/gpgme_11.html | 130 ++++ + pyme-web/doc/gpgme/gpgme_12.html | 82 +++ + pyme-web/doc/gpgme/gpgme_13.html | 130 ++++ + pyme-web/doc/gpgme/gpgme_14.html | 108 ++++ + pyme-web/doc/gpgme/gpgme_15.html | 69 +++ + pyme-web/doc/gpgme/gpgme_16.html | 169 +++++ + pyme-web/doc/gpgme/gpgme_17.html | 63 ++ + pyme-web/doc/gpgme/gpgme_18.html | 63 ++ + pyme-web/doc/gpgme/gpgme_19.html | 66 ++ + pyme-web/doc/gpgme/gpgme_2.html | 79 +++ + pyme-web/doc/gpgme/gpgme_20.html | 120 ++++ + pyme-web/doc/gpgme/gpgme_21.html | 102 +++ + pyme-web/doc/gpgme/gpgme_22.html | 108 ++++ + pyme-web/doc/gpgme/gpgme_23.html | 237 +++++++ + pyme-web/doc/gpgme/gpgme_24.html | 154 +++++ + pyme-web/doc/gpgme/gpgme_25.html | 248 ++++++++ + pyme-web/doc/gpgme/gpgme_26.html | 107 ++++ + pyme-web/doc/gpgme/gpgme_27.html | 80 +++ + pyme-web/doc/gpgme/gpgme_28.html | 67 ++ + pyme-web/doc/gpgme/gpgme_29.html | 164 +++++ + pyme-web/doc/gpgme/gpgme_3.html | 86 +++ + pyme-web/doc/gpgme/gpgme_30.html | 106 ++++ + pyme-web/doc/gpgme/gpgme_31.html | 232 +++++++ + pyme-web/doc/gpgme/gpgme_32.html | 85 +++ + pyme-web/doc/gpgme/gpgme_33.html | 223 +++++++ + pyme-web/doc/gpgme/gpgme_34.html | 83 +++ + pyme-web/doc/gpgme/gpgme_35.html | 70 +++ + pyme-web/doc/gpgme/gpgme_36.html | 63 ++ + pyme-web/doc/gpgme/gpgme_37.html | 66 ++ + pyme-web/doc/gpgme/gpgme_38.html | 86 +++ + pyme-web/doc/gpgme/gpgme_39.html | 79 +++ + pyme-web/doc/gpgme/gpgme_4.html | 83 +++ + pyme-web/doc/gpgme/gpgme_40.html | 89 +++ + pyme-web/doc/gpgme/gpgme_41.html | 99 +++ + pyme-web/doc/gpgme/gpgme_42.html | 144 +++++ + pyme-web/doc/gpgme/gpgme_43.html | 152 +++++ + pyme-web/doc/gpgme/gpgme_44.html | 112 ++++ + pyme-web/doc/gpgme/gpgme_45.html | 101 +++ + pyme-web/doc/gpgme/gpgme_46.html | 459 ++++++++++++++ + pyme-web/doc/gpgme/gpgme_47.html | 292 +++++++++ + pyme-web/doc/gpgme/gpgme_48.html | 363 +++++++++++ + pyme-web/doc/gpgme/gpgme_49.html | 209 +++++++ + pyme-web/doc/gpgme/gpgme_5.html | 74 +++ + pyme-web/doc/gpgme/gpgme_50.html | 88 +++ + pyme-web/doc/gpgme/gpgme_51.html | 208 +++++++ + pyme-web/doc/gpgme/gpgme_52.html | 154 +++++ + pyme-web/doc/gpgme/gpgme_53.html | 291 +++++++++ + pyme-web/doc/gpgme/gpgme_54.html | 91 +++ + pyme-web/doc/gpgme/gpgme_55.html | 107 ++++ + pyme-web/doc/gpgme/gpgme_56.html | 140 +++++ + pyme-web/doc/gpgme/gpgme_57.html | 106 ++++ + pyme-web/doc/gpgme/gpgme_58.html | 89 +++ + pyme-web/doc/gpgme/gpgme_59.html | 97 +++ + pyme-web/doc/gpgme/gpgme_6.html | 77 +++ + pyme-web/doc/gpgme/gpgme_60.html | 142 +++++ + pyme-web/doc/gpgme/gpgme_61.html | 626 +++++++++++++++++++ + pyme-web/doc/gpgme/gpgme_62.html | 107 ++++ + pyme-web/doc/gpgme/gpgme_63.html | 67 ++ + pyme-web/doc/gpgme/gpgme_64.html | 95 +++ + pyme-web/doc/gpgme/gpgme_65.html | 233 +++++++ + pyme-web/doc/gpgme/gpgme_66.html | 65 ++ + pyme-web/doc/gpgme/gpgme_67.html | 220 +++++++ + pyme-web/doc/gpgme/gpgme_68.html | 75 +++ + pyme-web/doc/gpgme/gpgme_69.html | 119 ++++ + pyme-web/doc/gpgme/gpgme_7.html | 123 ++++ + pyme-web/doc/gpgme/gpgme_70.html | 107 ++++ + pyme-web/doc/gpgme/gpgme_71.html | 218 +++++++ + pyme-web/doc/gpgme/gpgme_72.html | 134 ++++ + pyme-web/doc/gpgme/gpgme_73.html | 299 +++++++++ + pyme-web/doc/gpgme/gpgme_74.html | 103 ++++ + pyme-web/doc/gpgme/gpgme_75.html | 104 ++++ + pyme-web/doc/gpgme/gpgme_76.html | 118 ++++ + pyme-web/doc/gpgme/gpgme_77.html | 95 +++ + pyme-web/doc/gpgme/gpgme_78.html | 71 +++ + pyme-web/doc/gpgme/gpgme_79.html | 686 +++++++++++++++++++++ + pyme-web/doc/gpgme/gpgme_8.html | 155 +++++ + pyme-web/doc/gpgme/gpgme_80.html | 120 ++++ + pyme-web/doc/gpgme/gpgme_81.html | 278 +++++++++ + pyme-web/doc/gpgme/gpgme_82.html | 272 ++++++++ + pyme-web/doc/gpgme/gpgme_83.html | 180 ++++++ + pyme-web/doc/gpgme/gpgme_84.html | 99 +++ + pyme-web/doc/gpgme/gpgme_9.html | 104 ++++ + pyme-web/doc/gpgme/gpgme_abt.html | 206 +++++++ + pyme-web/doc/gpgme/gpgme_fot.html | 53 ++ + pyme-web/doc/gpgme/gpgme_ovr.html | 68 ++ + pyme-web/doc/gpgme/gpgme_toc.html | 247 ++++++++ + pyme-web/doc/gpgme/index.html | 251 ++++++++ + pyme-web/doc/pyme/index.html | 166 +++++ + pyme-web/doc/pyme/pyme.callbacks.html | 50 ++ + .../doc/pyme/pyme.constants.data.encoding.html | 48 ++ + pyme-web/doc/pyme/pyme.constants.data.html | 29 + + pyme-web/doc/pyme/pyme.constants.event.html | 48 ++ + pyme-web/doc/pyme/pyme.constants.html | 39 ++ + pyme-web/doc/pyme/pyme.constants.import.html | 49 ++ + pyme-web/doc/pyme/pyme.constants.keylist.html | 29 + + pyme-web/doc/pyme/pyme.constants.keylist.mode.html | 48 ++ + pyme-web/doc/pyme/pyme.constants.md.html | 58 ++ + pyme-web/doc/pyme/pyme.constants.pk.html | 50 ++ + pyme-web/doc/pyme/pyme.constants.protocol.html | 46 ++ + pyme-web/doc/pyme/pyme.constants.sig.html | 29 + + pyme-web/doc/pyme/pyme.constants.sig.mode.html | 47 ++ + pyme-web/doc/pyme/pyme.constants.sigsum.html | 55 ++ + pyme-web/doc/pyme/pyme.constants.status.html | 117 ++++ + pyme-web/doc/pyme/pyme.constants.validity.html | 50 ++ + pyme-web/doc/pyme/pyme.core.html | 254 ++++++++ + pyme-web/doc/pyme/pyme.errors.html | 90 +++ + pyme-web/doc/pyme/pyme.html | 166 +++++ + pyme-web/doc/pyme/pyme.util.html | 78 +++ + pyme-web/doc/pyme/pyme.version.html | 37 ++ + pyme-web/index.html | 6 +- + 113 files changed, 14966 insertions(+), 1 deletion(-) + +commit 2d6fe54479f042644f7b0f3d2fe35877d2056144 +Author: belyi <devnull@localhost> +Date: Thu May 19 02:06:09 2005 +0000 + + Added INSTALL file. + + pyme/INSTALL | 10 ++++++++++ + 1 file changed, 10 insertions(+) + +commit d6892fff0c3cedf41dba4c25ab8608e7f2bc039c +Author: belyi <devnull@localhost> +Date: Tue May 17 16:49:28 2005 +0000 + + Update copyright note on simple.py + + pyme/examples/simple.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +commit c2cd9cdf5995843aad7b200b929db2969effc9d2 +Author: belyi <devnull@localhost> +Date: Tue May 17 15:03:58 2005 +0000 + + Update simple.py to catch errors. + + pyme/examples/simple.py | 17 +++++++++++------ + 1 file changed, 11 insertions(+), 6 deletions(-) + +commit eaedae7c6a0ea993caab067efe781a59b6769c44 +Author: belyi <devnull@localhost> +Date: Tue May 17 01:18:23 2005 +0000 + + Added 'PYTHON = python' into Makefile for bug #1199122 + + pyme/Makefile | 1 + + pyme/examples/signverify.py | 1 + + 2 files changed, 2 insertions(+) + +commit 56fd244bb2636a4d58629899ea3cde1d96428198 +Author: belyi <devnull@localhost> +Date: Wed Apr 27 21:37:06 2005 +0000 + + Added pygpa example. + + pyme/debian/changelog | 3 +- + pyme/examples/pygpa.glade | 5546 +++++++++++++++++++++++++++++++++++++++++++++ + pyme/examples/pygpa.py | 1459 ++++++++++++ + 3 files changed, 7007 insertions(+), 1 deletion(-) + +commit 2d9a2a91a59ac3fee5410c953b7e0859e9e7cd35 +Author: belyi <devnull@localhost> +Date: Thu Apr 21 15:17:51 2005 +0000 + + Change version to 0.7.0 due to the change in license. + + pyme/debian/changelog | 2 +- + pyme/pyme/version.py | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +commit 94e34e38d742f145385bd235825b6ba1e30d8339 +Author: belyi <devnull@localhost> +Date: Thu Apr 21 03:53:12 2005 +0000 + + Changed license on PyMe from GPL to LGPL. + PyMe examples keep GPL license. + + pyme/COPYING.LESSER | 510 +++++++++++++++++++++++++++++++++++ + pyme/Makefile | 20 +- + pyme/debian/changelog | 4 +- + pyme/debian/copyright | 22 +- + pyme/gpgme-h-clean.py | 16 ++ + pyme/gpgme.i | 20 +- + pyme/helpers.c | 20 +- + pyme/helpers.h | 20 +- + pyme/pyme/__init__.py | 20 +- + pyme/pyme/callbacks.py | 20 +- + pyme/pyme/constants/data/encoding.py | 20 +- + pyme/pyme/constants/event.py | 20 +- + pyme/pyme/constants/import.py | 20 +- + pyme/pyme/constants/keylist/mode.py | 20 +- + pyme/pyme/constants/md.py | 20 +- + pyme/pyme/constants/pk.py | 20 +- + pyme/pyme/constants/protocol.py | 20 +- + pyme/pyme/constants/sig/mode.py | 20 +- + pyme/pyme/constants/sigsum.py | 20 +- + pyme/pyme/constants/status.py | 20 +- + pyme/pyme/constants/validity.py | 20 +- + pyme/pyme/core.py | 20 +- + pyme/pyme/errors.py | 20 +- + pyme/pyme/util.py | 20 +- + pyme/pyme/version.py | 22 +- + pyme/setup.py | 20 +- + 26 files changed, 761 insertions(+), 233 deletions(-) + +commit 0d8aa0f6335cb1506a37085095ed45173b099a02 +Author: belyi <devnull@localhost> +Date: Tue Apr 19 01:46:06 2005 +0000 + + Added __hash__ and __eq__ methods to GpgmeWrapper to allow both Context() + and Data() to be used as a dictionary key. + Changed core.wait() function to always return a tuple. On timeout now it + returns (0, None) instead of just None. Plus, return context is now a + Context() object instead of a wrapper return by underlying gpgme. + + pyme/helpers.c | 1 - + pyme/pyme/core.py | 25 +++++++++++++++---------- + pyme/pyme/util.py | 9 +++++++++ + 3 files changed, 24 insertions(+), 11 deletions(-) + +commit 63ff6d10637be1dcbcd78c939ac1ef1ac30b1024 +Author: belyi <devnull@localhost> +Date: Wed Apr 6 04:58:40 2005 +0000 + + Made hook parameter optional in passphrase_cb and progress_cb. + Allowed None for callbacks to unset ones set previously. + Removed cleanup of exception in callbacks - now just retrieve the error code. + Added prev_bad parameter in passphrase_cb since it can be used in + change password protocols. + Updated examples to follow new sets of arguments in callbacks + Updated op_edit to check if passed key is None (otherwise gpgme dumps core) + God rid of annoying warning "function declaration isn't a prototype" in + helpers.c and helpers.h by changing from () to (void) list of arguments. + + pyme/debian/changelog | 10 +++++--- + pyme/examples/signverify.py | 2 +- + pyme/examples/t-edit.py | 2 +- + pyme/gpgme.i | 18 +++++++++----- + pyme/helpers.c | 60 ++++++++++++++++++++++++++++++--------------- + pyme/helpers.h | 4 +-- + pyme/pyme/callbacks.py | 6 +++-- + pyme/pyme/core.py | 47 +++++++++++++++++++++-------------- + pyme/pyme/errors.py | 2 +- + 9 files changed, 96 insertions(+), 55 deletions(-) + +commit 8f0ab8138c7aa190936376ccbbf33bb09c64d6f1 +Author: belyi <devnull@localhost> +Date: Thu Mar 31 23:50:59 2005 +0000 + + Added exception handling in passphrase_cb and edit_cb. If GPGMEError + exception is thrown in those callbacks it will be converted into its + core representation and return as an error code to the caller. + On all other exceptions error code will be GPG_ERR_GENERAL. + + pyme/Makefile | 1 + + pyme/debian/changelog | 8 ++++++++ + pyme/gpgme.i | 20 ++++++++++++++------ + pyme/helpers.c | 51 +++++++++++++++++++++++++++++++++++++++++++++------ + pyme/helpers.h | 3 +++ + 5 files changed, 71 insertions(+), 12 deletions(-) + +commit 9903d1fb11231e7e3d920e58d1ecb674c5988b07 +Author: belyi <devnull@localhost> +Date: Thu Mar 31 05:12:15 2005 +0000 + + Remove workaround from Context.wait() method since the bug report and + patch fixing gpgme_wait's behavior is sent to GPMGE developers already. + Added errorcheck into op_edit() so that it can report an error. + + pyme/pyme/core.py | 10 +++++----- + 1 file changed, 5 insertions(+), 5 deletions(-) + +commit 45e8a5f4e13d3ca797ec3b0037242874a6be5562 +Author: belyi <devnull@localhost> +Date: Sat Mar 26 19:44:18 2005 +0000 + + Updated verion number to 0.6.2 in version.py + Added examples/*.glade files into documentation package. + + pyme/debian/examples | 1 + + pyme/pyme/version.py | 2 +- + 2 files changed, 2 insertions(+), 1 deletion(-) + +commit 270b87bb40e180cb6e8f1de9a0e8161525ffa4ab +Author: belyi <devnull@localhost> +Date: Sat Mar 26 19:31:14 2005 +0000 + + Updated debian/changelog regarding PyGtkGpgKeys example and a fix in errors. + + pyme/debian/changelog | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +commit ea4682009a506db91e5174ffd038fe7e4406b591 +Author: belyi <devnull@localhost> +Date: Sat Mar 26 19:25:36 2005 +0000 + + Added handling of right mouse button click. + Changed reporting a string instead of a number on key generation failure. + + pyme/examples/PyGtkGpgKeys.glade | 2 ++ + pyme/examples/PyGtkGpgKeys.py | 30 +++++++++++++++++++++++++++--- + 2 files changed, 29 insertions(+), 3 deletions(-) + +commit f65ad1a703d0098a3204fb8527a54d253e5847e7 +Author: belyi <devnull@localhost> +Date: Sat Mar 26 18:11:11 2005 +0000 + + Added another column indicating if a key has a secret part. + Automated generation of the View menu from the view field of the KeyColumn + class. + + pyme/examples/PyGtkGpgKeys.glade | 93 ++-------------------------------------- + pyme/examples/PyGtkGpgKeys.py | 74 +++++++++++++++++--------------- + 2 files changed, 44 insertions(+), 123 deletions(-) + +commit b54e83a7a7a5785502f3c7e8b95f15e23b40e65a +Author: belyi <devnull@localhost> +Date: Sat Mar 26 16:45:13 2005 +0000 + + Small change to the way gtk.TreeModel object is used. + + pyme/examples/PyGtkGpgKeys.py | 21 ++++++++++----------- + 1 file changed, 10 insertions(+), 11 deletions(-) + +commit 7078db75cef4c1fd70cf03e37172bdb4f933fd1b +Author: belyi <devnull@localhost> +Date: Fri Mar 25 23:33:06 2005 +0000 + + Use more comprehansible error reporting since gpgme_strerror_r returns None + all the time. + + pyme/pyme/errors.py | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +commit 151213f4344d9984975721440af07de09e3df61c +Author: belyi <devnull@localhost> +Date: Fri Mar 25 04:30:17 2005 +0000 + + Improved PyGtkGpgKeys example to manage owner_trust on keys. + Added another example inter-edit.py which is just a hepler to write + scripts for Context.op_edit() command. + + pyme/examples/PyGtkGpgKeys.glade | 78 ++++++++++++++++++++++++++++++++++++++++ + pyme/examples/PyGtkGpgKeys.py | 68 +++++++++++++++++++++++++++++++---- + pyme/examples/inter-edit.py | 54 ++++++++++++++++++++++++++++ + pyme/examples/t-edit.py | 18 ++++++++++ + 4 files changed, 212 insertions(+), 6 deletions(-) + +commit fc7235af217bcee5231ce7fbd7f234712d5ad3b0 +Author: belyi <devnull@localhost> +Date: Fri Mar 25 00:30:39 2005 +0000 + + Updated PyGtkGpgKeys example to include import, export and reload + functionality. Also added ability to remove number of keys simultanously. + Rearanged how KeyColumn is used to avoid unnecessary sorts and duplication + of information in different parts of the code. + + pyme/examples/PyGtkGpgKeys.glade | 86 +++++++++- + pyme/examples/PyGtkGpgKeys.py | 332 ++++++++++++++++++++++++++++----------- + 2 files changed, 325 insertions(+), 93 deletions(-) + +commit 9f65749ccb1b7cab562e19c03f4371d5f7d94912 +Author: belyi <devnull@localhost> +Date: Thu Mar 24 05:51:03 2005 +0000 + + Added example of PyGTK+ and PyMe integration. + For now it does only simple things - listing, deleting, and generating keys. + + pyme/examples/PyGtkGpgKeys.glade | 1321 +++++++++++++++++++++++++++++++++++++ + pyme/examples/PyGtkGpgKeys.gladep | 8 + + pyme/examples/PyGtkGpgKeys.py | 424 ++++++++++++ + 3 files changed, 1753 insertions(+) + +commit 59e23f32c3b46413c9ec09e23e1a385a110fb103 +Author: belyi <devnull@localhost> +Date: Thu Mar 24 05:44:58 2005 +0000 + + Added wait method Context class which handles asynchornous calls a little + bit better than the one generated by SWIG. + + pyme/debian/changelog | 7 +++++++ + pyme/gpgme.i | 1 + + pyme/pyme/core.py | 40 ++++++++++++++++++++++++++++++++++++++++ + 3 files changed, 48 insertions(+) + +commit 4c1b5259e4985df2cba0ae4fc09f12cd94603a75 +Author: belyi <devnull@localhost> +Date: Tue Mar 22 18:29:31 2005 +0000 + + Added correct handling of Context.op_edit() method. + Added example/t-edit.py showing usage for this method. + Output of this example should match output of the tests/gpg/t-edit + from the GPGME test suite. + Remove unused static function from helpers.c + + pyme/examples/t-edit.py | 38 ++++++++++++++++++++++++++++++++++++++ + pyme/gpgme.i | 36 ++++++++++++++++++++++++++++++++++++ + pyme/helpers.c | 36 ------------------------------------ + pyme/pyme/core.py | 5 ++++- + 4 files changed, 78 insertions(+), 37 deletions(-) + +commit dc587e215283bfef2dd594f86a7b2945f74f5155 +Author: belyi <devnull@localhost> +Date: Sat Mar 19 01:43:59 2005 +0000 + + Update changelog to include note about deprecated function in 0.6.1 release + + pyme/debian/changelog | 3 ++- + pyme/examples/encrypt-to-all.py | 3 +-- + 2 files changed, 3 insertions(+), 3 deletions(-) + +commit 86de4b3ad777f980ccf7ba3462c85bbe1787d1fd +Author: belyi <devnull@localhost> +Date: Sat Mar 19 01:40:07 2005 +0000 + + Remove deprecated functions from helpers.[ch] + Use gpgme-h-clean.py to remove deprecated functions and typedefs from + the GPGME header file. This will reduce the number of unused methods. + + pyme/Makefile | 4 ++-- + pyme/gpgme-h-clean.py | 26 ++++++++++++++++++++++++++ + pyme/helpers.c | 8 -------- + pyme/helpers.h | 2 -- + 4 files changed, 28 insertions(+), 12 deletions(-) + +commit 2483efcbd0d73c628c4d7717928a766c3b58f0aa +Author: belyi <devnull@localhost> +Date: Fri Mar 18 22:15:52 2005 +0000 + + Update copyright and author values in pyme/version.py + Create rules to build distribution files - one full and one without + debian bits. + + pyme/Makefile | 28 ++++++++++++++++++++++------ + pyme/pyme/version.py | 12 ++++++------ + 2 files changed, 28 insertions(+), 12 deletions(-) + +commit 168593285380f5a7805f3dd08657d429a72d3621 +Author: belyi <devnull@localhost> +Date: Fri Mar 18 19:09:33 2005 +0000 + + Added package building for python2.4 + + Updated copyright notes to include myslef and avoid confusion who's the + maintainer. In John's own words: "I'd prefer to just step out of the picture". + Jonh's copyright notice left intact. + + pyme/Makefile | 6 +++--- + pyme/debian/changelog | 7 +++++++ + pyme/debian/control | 30 +++++++++++++++++++++++++++--- + pyme/debian/copyright | 10 ++++------ + pyme/debian/rules | 4 ++++ + pyme/debian/setup.cfg-2.4 | 8 ++++++++ + pyme/examples/genkey.py | 4 ++-- + pyme/gpgme.i | 4 ++-- + pyme/helpers.c | 4 ++-- + pyme/helpers.h | 4 ++-- + pyme/pyme/__init__.py | 4 ++-- + pyme/pyme/callbacks.py | 4 ++-- + pyme/pyme/constants/data/encoding.py | 4 ++-- + pyme/pyme/constants/event.py | 4 ++-- + pyme/pyme/constants/import.py | 4 ++-- + pyme/pyme/constants/keylist/mode.py | 4 ++-- + pyme/pyme/constants/md.py | 4 ++-- + pyme/pyme/constants/pk.py | 4 ++-- + pyme/pyme/constants/protocol.py | 4 ++-- + pyme/pyme/constants/sig/mode.py | 4 ++-- + pyme/pyme/constants/sigsum.py | 4 ++-- + pyme/pyme/constants/status.py | 4 ++-- + pyme/pyme/constants/validity.py | 4 ++-- + pyme/pyme/core.py | 4 ++-- + pyme/pyme/errors.py | 4 ++-- + pyme/pyme/util.py | 4 ++-- + pyme/pyme/version.py | 2 +- + pyme/setup.py | 3 ++- + 28 files changed, 96 insertions(+), 54 deletions(-) + +commit 6dbbb252771133724b2879ed6d767cd708196dae +Author: belyi <devnull@localhost> +Date: Fri Mar 18 18:04:35 2005 +0000 + + Remove the note about gpgme.i to be generated - it's been the primary source + for some time. + + pyme/gpgme.i | 6 ------ + 1 file changed, 6 deletions(-) + +commit 9d449fa4889c6bda6d14583c0625b8d5c4ffe759 +Author: belyi <devnull@localhost> +Date: Fri May 7 18:31:22 2004 +0000 + + Added my copyright in genkey.py since there's enough changes made. + Updated signverify to use only keys generated by genkey.py, to check + that keys added to singers are able to sign and to check that the + list of signers is not empty. The last check is necessary to prevent + signing with the key of the user running signverify.py script. + Added delkey.py script to delete keys generated by genkey.py + Added exportimport.py example for key export/import. + + pyme/examples/delkey.py | 29 +++++++++++++++++ + pyme/examples/exportimport.py | 76 +++++++++++++++++++++++++++++++++++++++++++ + pyme/examples/genkey.py | 6 ++-- + pyme/examples/signverify.py | 18 ++++++---- + 4 files changed, 119 insertions(+), 10 deletions(-) + +commit df98c8d28245ad2c14b0ab50fc8f8932853bec8b +Author: belyi <devnull@localhost> +Date: Tue May 4 17:34:15 2004 +0000 + + Added examples/signverify.py for unattended sing/verify. + Updated examples/genkey.py to work correctly. + Updated gpgme.i to allow None as a value for gpgme_data_t + + pyme/examples/genkey.py | 14 ++------- + pyme/examples/signverify.py | 72 +++++++++++++++++++++++++++++++++++++++++++++ + pyme/gpgme.i | 21 ++++++++----- + 3 files changed, 87 insertions(+), 20 deletions(-) + +commit ba45931abf530ab89ead46d7233ff1b62b629a18 +Author: belyi <devnull@localhost> +Date: Thu Apr 8 16:15:09 2004 +0000 + + Ensure that we support only python2.2 and up. :-) + Use generators in core.Context class which makes pyme.aux obsolete + Remove importing future nested_scopes since they are standart starting + with python2.2 + + pyme/pyme/__init__.py | 5 ++--- + pyme/pyme/aux.py | 56 --------------------------------------------------- + pyme/pyme/core.py | 15 +++++++++++--- + pyme/pyme/errors.py | 1 - + pyme/pyme/util.py | 2 +- + 5 files changed, 15 insertions(+), 64 deletions(-) + +commit 4e9be5a55ecffa4da7ad5c192cc892eddaaa9586 +Author: belyi <devnull@localhost> +Date: Sun Mar 21 03:53:30 2004 +0000 + + Small change to index.html + Added clean: rule to the Makefile + + pyme-web/Makefile | 3 +++ + pyme-web/index.html | 6 +++--- + 2 files changed, 6 insertions(+), 3 deletions(-) + +commit 2efb95176f4edf56ed61c9ac0c3aa09c56534df0 +Author: belyi <devnull@localhost> +Date: Sun Mar 21 03:00:32 2004 +0000 + + Added Makefile rules for pyme module installation. + + pyme/Makefile | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + +commit 2b83d5d8b513029cc3e54f2fa502ccc85618104b +Author: belyi <devnull@localhost> +Date: Sun Mar 21 02:29:54 2004 +0000 + + Decorative change. + + pyme/pyme/aux.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit e3478015d763a036c1d806ae01433fce59712204 +Author: belyi <devnull@localhost> +Date: Sun Mar 21 02:25:55 2004 +0000 + + Added RCS Id: tags + + pyme/Makefile | 1 + + pyme/examples/encrypt-to-all.py | 3 ++- + pyme/examples/genkey.py | 3 ++- + pyme/examples/sign.py | 3 ++- + pyme/examples/simple.py | 3 ++- + pyme/gpgme.i | 1 + + pyme/helpers.c | 1 + + pyme/helpers.h | 1 + + pyme/pyme/__init__.py | 1 + + pyme/pyme/aux.py | 1 + + pyme/pyme/callbacks.py | 1 + + pyme/pyme/constants/__init__.py | 2 ++ + pyme/pyme/constants/data/__init__.py | 2 ++ + pyme/pyme/constants/data/encoding.py | 1 + + pyme/pyme/constants/event.py | 1 + + pyme/pyme/constants/import.py | 1 + + pyme/pyme/constants/keylist/__init__.py | 2 ++ + pyme/pyme/constants/keylist/mode.py | 1 + + pyme/pyme/constants/md.py | 1 + + pyme/pyme/constants/pk.py | 1 + + pyme/pyme/constants/protocol.py | 1 + + pyme/pyme/constants/sig/__init__.py | 2 ++ + pyme/pyme/constants/sig/mode.py | 1 + + pyme/pyme/constants/sigsum.py | 1 + + pyme/pyme/constants/status.py | 1 + + pyme/pyme/constants/validity.py | 1 + + pyme/pyme/core.py | 1 + + pyme/pyme/errors.py | 1 + + pyme/pyme/util.py | 1 + + pyme/pyme/version.py | 2 ++ + 30 files changed, 39 insertions(+), 4 deletions(-) + +commit b3b3712645332c5bc3e8d9d557aab21d48ff0f86 +Author: belyi <devnull@localhost> +Date: Sun Mar 21 02:07:36 2004 +0000 + + Added Id: RCS tags to all files. + + pyme-web/Makefile | 2 ++ + pyme-web/index.html | 3 ++- + 2 files changed, 4 insertions(+), 1 deletion(-) + +commit 6aea2426beaaa8c43e6f2310a37a2737c0c3a1b5 +Author: belyi <devnull@localhost> +Date: Sun Mar 21 01:50:55 2004 +0000 + + Update example on the init pyme.html page to match simple.py example. + Fix core.py to use getcode() instead of getvalue() method of the exception. + + pyme/pyme/__init__.py | 22 ++++++++++++++-------- + pyme/pyme/core.py | 4 ++-- + 2 files changed, 16 insertions(+), 10 deletions(-) + +commit dee337455ffd624d3f83e1c159c4bb2cefc692c9 +Author: belyi <devnull@localhost> +Date: Sat Mar 20 20:32:29 2004 +0000 + + Added Makefile to simplify publishing web files. + + pyme-web/Makefile | 7 +++++++ + 1 file changed, 7 insertions(+) + +commit af7129baa8260697d85c2ddb434562e8a80b62d8 +Author: belyi <devnull@localhost> +Date: Sat Mar 20 20:15:53 2004 +0000 + + Added minimum of formating and SF icon. + + pyme-web/index.html | 18 +++++++++++------- + 1 file changed, 11 insertions(+), 7 deletions(-) + +commit 2e64dcbf99cee796b51667b04d8961e390edde87 +Author: belyi <devnull@localhost> +Date: Sat Mar 20 18:30:09 2004 +0000 + + Initial revision + + pyme-web/index.html | 33 +++++++++++++++++++++++++++++++++ + 1 file changed, 33 insertions(+) + +commit 1c51644b3d0b6611422d971758e35f303d2ad5df +Author: belyi <devnull@localhost> +Date: Sat Mar 20 05:10:46 2004 +0000 + + Update examples and package information on the initial pyme doc page. + + pyme/pyme/__init__.py | 27 ++++++++++++--------------- + 1 file changed, 12 insertions(+), 15 deletions(-) + +commit b2d31b0bfbffdff5247d6db4e3c95140cc1b1f19 +Author: belyi <devnull@localhost> +Date: Sat Mar 20 04:47:42 2004 +0000 + + Deleted unnecessary files. + Updated debian/control to remove dependency on python-xml package since there's + none now. + Move example files from 'doc' into separate control file. + Update debian/rules to build documentation from *.py files and to exclude + CVS directories from the installation. + + pyme/Makefile | 26 ++----- + pyme/debian/control | 8 +-- + pyme/debian/docs | 1 - + pyme/debian/ex.package.doc-base | 22 ------ + pyme/debian/examples | 1 + + pyme/debian/manpage.1.ex | 60 ---------------- + pyme/debian/manpage.sgml.ex | 152 ---------------------------------------- + pyme/debian/rules | 12 ++-- + 8 files changed, 15 insertions(+), 267 deletions(-) + +commit 1b517dd9b82a433499b4696b06d94d756cd36e53 +Author: belyi <devnull@localhost> +Date: Sat Mar 20 02:59:15 2004 +0000 + + Remove doc/gpgme directory containing GPGME documentation since this belongs + to a different project. Need to add reference in our documentation. + + pyme/doc/gpgme/fdl.texi | 402 ------ + pyme/doc/gpgme/gpgme.texi | 3372 ------------------------------------------- + pyme/doc/gpgme/gpl.texi | 397 ----- + pyme/doc/gpgme/version.texi | 4 - + 4 files changed, 4175 deletions(-) + +commit 95d7d171da115a0fedfe2a4a7e5acc8aa408f673 +Author: belyi <devnull@localhost> +Date: Sat Mar 20 02:45:03 2004 +0000 + + Change debian/rules to generate files by swig during build and to cleanup + those files on 'clean' rule. + Plus, leave generated gpgme_wrap.c in the root directory instead of moving + it into subdirectory 'generated'. + + pyme/Makefile | 8 +++----- + pyme/debian/rules | 3 ++- + pyme/setup.py | 2 +- + 3 files changed, 6 insertions(+), 7 deletions(-) + +commit 545b3d90d445c5c78e8d72b2c1780863e02c789a +Author: belyi <devnull@localhost> +Date: Sat Mar 20 02:18:01 2004 +0000 + + Initial revision + + pyme/COPYING | 340 ++++ + pyme/ChangeLog | 802 ++++++++ + pyme/Makefile | 79 + + pyme/debian/README.Debian | 6 + + pyme/debian/changelog | 19 + + pyme/debian/control | 68 + + pyme/debian/copyright | 27 + + pyme/debian/dirs | 2 + + pyme/debian/docs | 2 + + pyme/debian/ex.package.doc-base | 22 + + pyme/debian/manpage.1.ex | 60 + + pyme/debian/manpage.sgml.ex | 152 ++ + pyme/debian/postinst.ex | 48 + + pyme/debian/postrm.ex | 38 + + pyme/debian/preinst.ex | 44 + + pyme/debian/prerm.ex | 39 + + pyme/debian/rules | 130 ++ + pyme/debian/setup.cfg-2.2 | 8 + + pyme/debian/setup.cfg-2.3 | 8 + + pyme/doc/gpgme/fdl.texi | 402 ++++ + pyme/doc/gpgme/gpgme.texi | 3372 +++++++++++++++++++++++++++++++ + pyme/doc/gpgme/gpl.texi | 397 ++++ + pyme/doc/gpgme/version.texi | 4 + + pyme/examples/encrypt-to-all.py | 63 + + pyme/examples/genkey.py | 55 + + pyme/examples/sign.py | 28 + + pyme/examples/simple.py | 44 + + pyme/gpgme.i | 191 ++ + pyme/helpers.c | 139 ++ + pyme/helpers.h | 29 + + pyme/pyme/__init__.py | 134 ++ + pyme/pyme/aux.py | 55 + + pyme/pyme/callbacks.py | 45 + + pyme/pyme/constants/__init__.py | 2 + + pyme/pyme/constants/data/__init__.py | 2 + + pyme/pyme/constants/data/encoding.py | 19 + + pyme/pyme/constants/event.py | 19 + + pyme/pyme/constants/import.py | 19 + + pyme/pyme/constants/keylist/__init__.py | 2 + + pyme/pyme/constants/keylist/mode.py | 19 + + pyme/pyme/constants/md.py | 19 + + pyme/pyme/constants/pk.py | 19 + + pyme/pyme/constants/protocol.py | 19 + + pyme/pyme/constants/sig/__init__.py | 2 + + pyme/pyme/constants/sig/mode.py | 19 + + pyme/pyme/constants/sigsum.py | 19 + + pyme/pyme/constants/status.py | 19 + + pyme/pyme/constants/validity.py | 19 + + pyme/pyme/core.py | 367 ++++ + pyme/pyme/errors.py | 46 + + pyme/pyme/util.py | 61 + + pyme/pyme/version.py | 39 + + pyme/setup.py | 60 + + 53 files changed, 7642 insertions(+) + +commit a3d5a442dc713b6c4d6fc4134db5b47e379dc41d +Author: root <devnull@localhost> +Date: Fri Mar 19 14:12:30 2004 +0000 + + initial checkin + + CVSROOT/checkoutlist | 13 +++++++++++++ + CVSROOT/commitinfo | 15 +++++++++++++++ + CVSROOT/config | 21 +++++++++++++++++++++ + CVSROOT/cvswrappers | 19 +++++++++++++++++++ + CVSROOT/editinfo | 21 +++++++++++++++++++++ + CVSROOT/loginfo | 26 ++++++++++++++++++++++++++ + CVSROOT/modules | 26 ++++++++++++++++++++++++++ + CVSROOT/notify | 12 ++++++++++++ + CVSROOT/rcsinfo | 13 +++++++++++++ + CVSROOT/taginfo | 20 ++++++++++++++++++++ + CVSROOT/verifymsg | 21 +++++++++++++++++++++ + 11 files changed, 207 insertions(+) diff --git a/lang/python/doc/rst/gpgme-python-howto.rst b/lang/python/doc/rst/gpgme-python-howto.rst new file mode 100644 index 0000000..9181491 --- /dev/null +++ b/lang/python/doc/rst/gpgme-python-howto.rst @@ -0,0 +1,2998 @@ +.. _intro: + +Introduction +============ + ++-----------------------------------+-----------------------------------+ +| Version: | 0.1.4 | ++-----------------------------------+-----------------------------------+ +| GPGME Version: | 1.12.0 | ++-----------------------------------+-----------------------------------+ +| Author: | `Ben | +| | McGinnes <https://gnupg.org/peopl | +| | e/index.html#sec-1-5>`__ | +| | <ben@gnupg.org> | ++-----------------------------------+-----------------------------------+ +| Author GPG Key: | DB4724E6FA4286C92B4E55C4321E4E237 | +| | 3590E5D | ++-----------------------------------+-----------------------------------+ +| Language: | Australian English, British | +| | English | ++-----------------------------------+-----------------------------------+ +| xml:lang: | en-AU, en-GB, en | ++-----------------------------------+-----------------------------------+ + +This document provides basic instruction in how to use the GPGME Python +bindings to programmatically leverage the GPGME library. + +.. _py2-vs-py3: + +Python 2 versus Python 3 +------------------------ + +Though the GPGME Python bindings themselves provide support for both +Python 2 and 3, the focus is unequivocally on Python 3 and specifically +from Python 3.4 and above. As a consequence all the examples and +instructions in this guide use Python 3 code. + +Much of it will work with Python 2, but much of it also deals with +Python 3 byte literals, particularly when reading and writing data. +Developers concentrating on Python 2.7, and possibly even 2.6, will need +to make the appropriate modifications to support the older string and +unicode types as opposed to bytes. + +There are multiple reasons for concentrating on Python 3; some of which +relate to the immediate integration of these bindings, some of which +relate to longer term plans for both GPGME and the python bindings and +some of which relate to the impending EOL period for Python 2.7. +Essentially, though, there is little value in tying the bindings to a +version of the language which is a dead end and the advantages offered +by Python 3 over Python 2 make handling the data types with which GPGME +deals considerably easier. + +.. _howto-python3-examples: + +Examples +-------- + +All of the examples found in this document can be found as Python 3 +scripts in the ``lang/python/examples/howto`` directory. + +Unofficial Drafts +----------------- + +In addition to shipping with each release of GPGME, there is a section +on locations to read or download `draft editions <#draft-editions>`__ of +this document from at the end of it. These are unofficial versions +produced in between major releases. + +.. _new-stuff: + +What\'s New +----------- + +The most obviously new point for those reading this guide is this +section on other new things, but that\'s hardly important. Not given all +the other things which spurred the need for adding this section and its +subsections. + +.. _new-stuff-1-12-0: + +New in GPGME 1·12·0 +~~~~~~~~~~~~~~~~~~~ + +There have been quite a number of additions to GPGME and the Python +bindings to it since the last release of GPGME with versions 1.11.0 and +1.11.1 in April, 2018. + +The bullet points of new additiions are: + +- an expanded section on `installing <#installation>`__ and + `troubleshooting <#snafu>`__ the Python bindings. +- The release of Python 3.7.0; which appears to be working just fine + with our bindings, in spite of intermittent reports of problems for + many other Python projects with that new release. +- Python 3.7 has been moved to the head of the specified python + versions list in the build process. +- In order to fix some other issues, there are certain underlying + functions which are more exposed through the + `gpg.Context() <#howto-get-context>`__, but ongoing documentation + ought to clarify that or otherwise provide the best means of using + the bindings. Some additions to ``gpg.core`` and the ``Context()``, + however, were intended (see below). +- Continuing work in identifying and confirming the cause of + oft-reported `problems installing the Python bindings on + Windows <#snafu-runtime-not-funtime>`__. +- GSOC: Google\'s Surreptitiously Ordered Conscription ... erm ... oh, + right; Google\'s Summer of Code. Though there were two hopeful + candidates this year; only one ended up involved with the GnuPG + Project directly, the other concentrated on an unrelated third party + project with closer ties to one of the GNU/Linux distributions than + to the GnuPG Project. Thus the Python bindings benefited from GSOC + participant Jacob Adams, who added the key\ :sub:`import` function; + building on prior work by Tobias Mueller. +- Several new methods functions were added to the gpg.Context(), + including: `key\ import <#howto-import-key>`__, + `key\ export <#howto-export-key>`__, + `key\ exportminimal <#howto-export-public-key>`__ and + `key\ exportsecret <#howto-export-secret-key>`__. +- Importing and exporting examples include versions integrated with + Marcel Fest\'s recently released `HKP for + Python <https://github.com/Selfnet/hkp4py>`__ module. Some + `additional notes on this module <#hkp4py>`__ are included at the end + of the HOWTO. +- Instructions for dealing with semi-walled garden implementations like + ProtonMail are also included. This is intended to make things a + little easier when communicating with users of ProtonMail\'s services + and should not be construed as an endorsement of said service. The + GnuPG Project neither favours, nor disfavours ProtonMail and the + majority of this deals with interacting with the ProtonMail + keyserver. +- Semi-formalised the location where `draft + versions <#draft-editions>`__ of this HOWTO may periodically be + accessible. This is both for the reference of others and testing the + publishing of the document itself. Renamed this file at around the + same time. +- The Texinfo documentation build configuration has been replicated + from the parent project in order to make to maintain consistency with + that project (and actually ship with each release). +- a reStructuredText (``.rst``) version is also generated for Python + developers more used to and comfortable with that format as it is the + standard Python documentation format and Python developers may wish + to use it with Sphinx. Please note that there has been no testing of + the reStructuredText version with Sphinx at all. The reST file was + generated by the simple expedient of using + `Pandoc <https://pandoc.org/>`__. +- Added a new section for `advanced or experimental + use <#advanced-use>`__. +- Began the advanced use cases with `a section <#cython>`__ on using + the module with `Cython <http://cython.org/>`__. +- Added a number of new scripts to the ``example/howto/`` directory; + some of which may be in advance of their planned sections of the + HOWTO (and some are just there because it seemed like a good idea at + the time). +- Cleaned up a lot of things under the hood. + +GPGME Concepts +============== + +.. _gpgme-c-api: + +A C API +------- + +Unlike many modern APIs with which programmers will be more familiar +with these days, the GPGME API is a C API. The API is intended for use +by C coders who would be able to access its features by including the +``gpgme.h`` header file with their own C source code and then access its +functions just as they would any other C headers. + +This is a very effective method of gaining complete access to the API +and in the most efficient manner possible. It does, however, have the +drawback that it cannot be directly used by other languages without some +means of providing an interface to those languages. This is where the +need for bindings in various languages stems. + +.. _gpgme-python-bindings: + +Python bindings +--------------- + +The Python bindings for GPGME provide a higher level means of accessing +the complete feature set of GPGME itself. It also provides a more +pythonic means of calling these API functions. + +The bindings are generated dynamically with SWIG and the copy of +``gpgme.h`` generated when GPGME is compiled. + +This means that a version of the Python bindings is fundamentally tied +to the exact same version of GPGME used to generate that copy of +``gpgme.h``. + +.. _gpgme-python-bindings-diffs: + +Difference between the Python bindings and other GnuPG Python packages +---------------------------------------------------------------------- + +There have been numerous attempts to add GnuPG support to Python over +the years. Some of the most well known are listed here, along with what +differentiates them. + +.. _diffs-python-gnupg: + +The python-gnupg package maintained by Vinay Sajip +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is arguably the most popular means of integrating GPG with Python. +The package utilises the ``subprocess`` module to implement wrappers for +the ``gpg`` and ``gpg2`` executables normally invoked on the command +line (``gpg.exe`` and ``gpg2.exe`` on Windows). + +The popularity of this package stemmed from its ease of use and +capability in providing the most commonly required features. + +Unfortunately it has been beset by a number of security issues in the +past; most of which stemmed from using unsafe methods of accessing the +command line via the ``subprocess`` calls. While some effort has been +made over the last two to three years (as of 2018) to mitigate this, +particularly by no longer providing shell access through those +subprocess calls, the wrapper is still somewhat limited in the scope of +its GnuPG features coverage. + +The python-gnupg package is available under the MIT license. + +.. _diffs-isis-gnupg: + +The gnupg package created and maintained by Isis Lovecruft +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In 2015 Isis Lovecruft from the Tor Project forked and then +re-implemented the python-gnupg package as just gnupg. This new package +also relied on subprocess to call the ``gpg`` or ``gpg2`` binaries, but +did so somewhat more securely. + +The naming and version numbering selected for this package, however, +resulted in conflicts with the original python-gnupg and since its +functions were called in a different manner to python-gnupg, the release +of this package also resulted in a great deal of consternation when +people installed what they thought was an upgrade that subsequently +broke the code relying on it. + +The gnupg package is available under the GNU General Public License +version 3.0 (or any later version). + +.. _diffs-pyme: + +The PyME package maintained by Martin Albrecht +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This package is the origin of these bindings, though they are somewhat +different now. For details of when and how the PyME package was folded +back into GPGME itself see the `Short History <short-history.org>`__ +document. [1]_ + +The PyME package was first released in 2002 and was also the first +attempt to implement a low level binding to GPGME. In doing so it +provided access to considerably more functionality than either the +``python-gnupg`` or ``gnupg`` packages. + +The PyME package is only available for Python 2.6 and 2.7. + +Porting the PyME package to Python 3.4 in 2015 is what resulted in it +being folded into the GPGME project and the current bindings are the end +result of that effort. + +The PyME package is available under the same dual licensing as GPGME +itself: the GNU General Public License version 2.0 (or any later +version) and the GNU Lesser General Public License version 2.1 (or any +later version). + +.. _gpgme-python-install: + +GPGME Python bindings installation +================================== + +.. _do-not-use-pypi: + +No PyPI +------- + +Most third-party Python packages and modules are available and +distributed through the Python Package Installer, known as PyPI. + +Due to the nature of what these bindings are and how they work, it is +infeasible to install the GPGME Python bindings in the same way. + +This is because the bindings use SWIG to dynamically generate C bindings +against ``gpgme.h`` and ``gpgme.h`` is generated from ``gpgme.h.in`` at +compile time when GPGME is built from source. Thus to include a package +in PyPI which actually built correctly would require either statically +built libraries for every architecture bundled with it or a full +implementation of C for each architecture. + +See the additional notes regarding `CFFI and SWIG <#snafu-cffi>`__ at +the end of this section for further details. + +.. _gpgme-python-requirements: + +Requirements +------------ + +The GPGME Python bindings only have three requirements: + +#. A suitable version of Python 2 or Python 3. With Python 2 that means + CPython 2.7 and with Python 3 that means CPython 3.4 or higher. +#. `SWIG <https://www.swig.org>`__. +#. GPGME itself. Which also means that all of GPGME\'s dependencies must + be installed too. + +.. _gpgme-python-recommendations: + +Recommended Additions +~~~~~~~~~~~~~~~~~~~~~ + +Though none of the following are absolute requirements, they are all +recommended for use with the Python bindings. In some cases these +recommendations refer to which version(s) of CPython to use the bindings +with, while others refer to third party modules which provide a +significant advantage in some way. + +#. If possible, use Python 3 instead of 2. +#. Favour a more recent version of Python since even 3.4 is due to reach + EOL soon. In production systems and services, Python 3.6 should be + robust enough to be relied on. +#. If possible add the following Python modules which are not part of + the standard library: + `Requests <http://docs.python-requests.org/en/latest/index.html>`__, + `Cython <http://cython.org/>`__ and + `hkp4py <https://github.com/Selfnet/hkp4py>`__. Chances are quite + high that at least the first one and maybe two of those will already + be installed. + +Note that, as with Cython, some of the planned additions to the +`Advanced <#advanced-use>`__ section, will bring with them additional +requirements. Most of these will be fairly well known and commonly +installed ones, however, which are in many cases likely to have already +been installed on many systems or be familiar to Python programmers. + +Installation +------------ + +Installing the Python bindings is effectively achieved by compiling and +installing GPGME itself. + +Once SWIG is installed with Python and all the dependencies for GPGME +are installed you only need to confirm that the version(s) of Python you +want the bindings installed for are in your ``$PATH``. + +By default GPGME will attempt to install the bindings for the most +recent or highest version number of Python 2 and Python 3 it detects in +``$PATH``. It specifically checks for the ``python`` and ``python3`` +executables first and then checks for specific version numbers. + +For Python 2 it checks for these executables in this order: ``python``, +``python2`` and ``python2.7``. + +For Python 3 it checks for these executables in this order: ``python3``, +``python3.7``, ``python3.6``, ``python3.5`` and ``python3.4``. [2]_ + +On systems where ``python`` is actually ``python3`` and not ``python2`` +it may be possible that ``python2`` may be overlooked, but there have +been no reports of that actually occurring as yet. + +In the three months or so since the release of Python 3.7.0 there has +been extensive testing and work with these bindings with no issues +specifically relating to the new version of Python or any of the new +features of either the language or the bindings. This has also been the +case with Python 3.7.1rc1. With that in mind and given the release of +Python 3.7.1 is scheduled for around the same time as GPGME 1.12.0, the +order of preferred Python versions has been changed to move Python 3.7 +ahead of Python 3.6. + +.. _install-gpgme: + +Installing GPGME +~~~~~~~~~~~~~~~~ + +See the GPGME ``README`` file for details of how to install GPGME from +source. + +.. _snafu: + +Known Issues +------------ + +There are a few known issues with the current build process and the +Python bindings. For the most part these are easily addressed should +they be encountered. + +.. _snafu-a-swig-of-this-builds-character: + +Breaking Builds +~~~~~~~~~~~~~~~ + +Occasionally when installing GPGME with the Python bindings included it +may be observed that the ``make`` portion of that process induces a +large very number of warnings and, eventually errors which end that part +of the build process. Yet following that with ``make check`` and +``make install`` appears to work seamlessly. + +The cause of this is related to the way SWIG needs to be called to +dynamically generate the C bindings for GPGME in the first place. So the +entire process will always produce ``lang/python/python2-gpg/`` and +``lang/python/python3-gpg/`` directories. These should contain the build +output generated during compilation, including the complete bindings and +module installed into ``site-packages``. + +Occasionally the errors in the early part or some other conflict (e.g. +not installing as **root** or **su**) may result in nothing being +installed to the relevant ``site-packages`` directory and the build +directory missing a lot of expected files. Even when this occurs, the +solution is actually quite simple and will always work. + +That solution is simply to run the following commands as either the +**root** user or prepended with ``sudo -H``\ [3]_ in the +``lang/python/`` directory: + +.. code:: shell + + /path/to/pythonX.Y setup.py build + /path/to/pythonX.Y setup.py build + /path/to/pythonX.Y setup.py install + +Yes, the build command does need to be run twice. Yes, you still need to +run the potentially failing or incomplete steps during the +``configure``, ``make`` and ``make install`` steps with installing +GPGME. This is because those steps generate a lot of essential files +needed, both by and in order to create, the bindings (including both the +``setup.py`` and ``gpgme.h`` files). + +#. IMPORTANT Note + + If specifying a selected number of languages to create bindings for, + try to leave Python last. Currently the majority of the other + language bindings are also preceding Python of either version when + listed alphabetically and so that just happens by default currently. + + If Python is set to precede one of the other languages then it is + possible that the errors described here may interrupt the build + process before generating bindings for those other languages. In + these cases it may be preferable to configure all preferred language + bindings separately with alternative ``configure`` steps for GPGME + using the ``--enable-languages=$LANGUAGE`` option. + +.. _snafu-lessons-for-the-lazy: + +Reinstalling Responsibly +~~~~~~~~~~~~~~~~~~~~~~~~ + +Regardless of whether you\'re installing for one version of Python or +several, there will come a point where reinstallation is required. With +most Python module installations, the installed files go into the +relevant site-packages directory and are then forgotten about. Then the +module is upgraded, the new files are copied over the old and that\'s +the end of the matter. + +While the same is true of these bindings, there have been intermittent +issues observed on some platforms which have benefited significantly +from removing all the previous installations of the bindings before +installing the updated versions. + +Removing the previous version(s) is simply a matter of changing to the +relevant ``site-packages`` directory for the version of Python in +question and removing the ``gpg/`` directory and any accompanying +egg-info files for that module. + +In most cases this will require root or administration privileges on the +system, but the same is true of installing the module in the first +place. + +.. _snafu-the-full-monty: + +Multiple installations +~~~~~~~~~~~~~~~~~~~~~~ + +For a veriety of reasons it may be either necessary or just preferable +to install the bindings to alternative installed Python versions which +meet the requirements of these bindings. + +On POSIX systems this will generally be most simply achieved by running +the manual installation commands (build, build, install) as described in +the previous section for each Python installation the bindings need to +be installed to. + +As per the SWIG documentation: the compilers, libraries and runtime used +to build GPGME and the Python Bindings **must** match those used to +compile Python itself, including the version number(s) (at least going +by major version numbers and probably minor numbers too). + +On most POSIX systems, including OS X, this will very likely be the case +in most, if not all, cases. + +.. _snafu-runtime-not-funtime: + +Won\'t Work With Windows +~~~~~~~~~~~~~~~~~~~~~~~~ + +There are semi-regular reports of Windows users having considerable +difficulty in installing and using the Python bindings at all. Very +often, possibly even always, these reports come from Cygwin users and/or +MinGW users and/or Msys2 users. Though not all of them have been +confirmed, it appears that these reports have also come from people who +installed Python using the Windows installer files from the `Python +website <https://python.org>`__ (i.e. mostly MSI installers, sometimes +self-extracting ``.exe`` files). + +The Windows versions of Python are not built using Cygwin, MinGW or +Msys2; they\'re built using Microsoft Visual Studio. Furthermore the +version used is *considerably* more advanced than the version which +MinGW obtained a small number of files from many years ago in order to +be able to compile anything at all. Not only that, but there are changes +to the version of Visual Studio between some micro releases, though that +is is particularly the case with Python 2.7, since it has been kept +around far longer than it should have been. + +There are two theoretical solutions to this issue: + +#. Compile and install the GnuPG stack, including GPGME and the Python + bibdings using the same version of Microsoft Visual Studio used by + the Python Foundation to compile the version of Python installed. + + If there are multiple versions of Python then this will need to be + done with each different version of Visual Studio used. + +#. Compile and install Python using the same tools used by choice, such + as MinGW or Msys2. + +Do **not** use the official Windows installer for Python unless +following the first method. + +In this type of situation it may even be for the best to accept that +there are less limitations on permissive software than free software and +simply opt to use a recent version of the Community Edition of Microsoft +Visual Studio to compile and build all of it, no matter what. + +Investigations into the extent or the limitations of this issue are +ongoing. + +.. _snafu-cffi: + +CFFI is the Best™ and GPGME should use it instead of SWIG +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There are many reasons for favouring +`CFFI <https://cffi.readthedocs.io/en/latest/overview.html>`__ and +proponents of it are quite happy to repeat these things as if all it +would take to switch from SWIG to CFFI is repeating that list as if it +were a new concept. + +The fact is that there are things which Python\'s CFFI implementation +cannot handle in the GPGME C code. Beyond that there are features of +SWIG which are simply not available with CFFI at all. SWIG generates the +bindings to Python using the ``gpgme.h`` file, but that file is not a +single version shipped with each release, it too is generated when GPGME +is compiled. + +CFFI is currently unable to adapt to such a potentially mutable +codebase. If there were some means of applying SWIG\'s dynamic code +generation to produce the Python/CFFI API modes of accessing the GPGME +libraries (or the source source code directly), but such a thing does +not exist yet either and it currently appears that work is needed in at +least one of CFFI\'s dependencies before any of this can be addressed. + +So if you\'re a massive fan of CFFI; that\'s great, but if you want this +project to switch to CFFI then rather than just insisting that it +should, I\'d suggest you volunteer to bring CFFI up to the level this +project needs. + +If you\'re actually seriously considering doing so, then I\'d suggest +taking the ``gpgme-tool.c`` file in the GPGME ``src/`` directory and +getting that to work with any of the CFFI API methods (not the ABI +methods, they\'ll work with pretty much anything). When you start +running into trouble with \"ifdefs\" then you\'ll know what sort of +things are lacking. That doesn\'t even take into account the amount of +work saved via SWIG\'s code generation techniques either. + +.. _snafu-venv: + +Virtualised Environments +~~~~~~~~~~~~~~~~~~~~~~~~ + +It is fairly common practice amongst Python developers to, as much as +possible, use packages like virtualenv to keep various things that are +to be installed from interfering with each other. Given how much of the +GPGME bindings is often at odds with the usual pythonic way of doing +things, it stands to reason that this would be called into question too. + +As it happens the answer as to whether or not the bindings can be used +with virtualenv, the answer is both yes and no. + +In general we recommend installing to the relevant path and matching +prefix of GPGME itself. Which means that when GPGME, and ideally the +rest of the GnuPG stack, is installed to a prefix like ``/usr/local`` or +``/opt/local`` then the bindings would need to be installed to the main +Python installation and not a virtualised abstraction. Attempts to +separate the two in the past have been known to cause weird and +intermittent errors ranging from minor annoyances to complete failures +in the build process. + +As a consequence we only recommend building with and installing to the +main Python installations within the same prefix as GPGME is installed +to or which are found by GPGME\'s configuration stage immediately prior +to running the make commands. Which is exactly what the compiling and +installing process of GPGME does by default. + +Once that is done, however, it appears that a copy the compiled module +may be installed into a virtualenv of the same major and minor version +matching the build. Alternatively it is possible to utilise a +``sites.pth`` file in the ``site-packages/`` directory of a viertualenv +installation, which links back to the system installations corresponding +directory in order to import anything installed system wide. This may or +may not be appropriate on a case by case basis. + +Though extensive testing of either of these options is not yet complete, +preliminary testing of them indicates that both are viable as long as +the main installation is complete. Which means that certain other +options normally restricted to virtual environments are also available, +including integration with pythonic test suites (e.g. +`pytest <https://docs.pytest.org/en/latest/index.html>`__) and other +large projects. + +That said, it is worth reiterating the warning regarding non-standard +installations. If one were to attempt to install the bindings only to a +virtual environment without somehow also including the full GnuPG stack +(or enough of it as to include GPGME) then it is highly likely that +errors would be encountered at some point and more than a little likely +that the build process itself would break. + +If a degree of separation from the main operating system is still +required in spite of these warnings, then consider other forms of +virtualisation. Either a virtual machine (e.g. +`VirtualBox <https://www.virtualbox.org/>`__), a hardware emulation +layer (e.g. `QEMU <https://www.qemu.org/>`__) or an application +container (e.g. `Docker <https://www.docker.com/why-docker>`__). + +Finally it should be noted that the limited tests conducted thus far +have been using the ``virtualenv`` command in a new directory to create +the virtual python environment. As opposed to the standard ``python3 +-m venv`` and it is possible that this will make a difference depending +on the system and version of Python in use. Another option is to run the +command ``python3 -m virtualenv /path/to/install/virtual/thingy`` +instead. + +.. _howto-fund-a-mental: + +Fundamentals +============ + +Before we can get to the fun stuff, there are a few matters regarding +GPGME\'s design which hold true whether you\'re dealing with the C code +directly or these Python bindings. + +.. _no-rest-for-the-wicked: + +No REST +------- + +The first part of which is or will be fairly blatantly obvious upon +viewing the first example, but it\'s worth reiterating anyway. That +being that this API is **not** a REST API. Nor indeed could it ever be +one. + +Most, if not all, Python programmers (and not just Python programmers) +know how easy it is to work with a RESTful API. In fact they\'ve become +so popular that many other APIs attempt to emulate REST-like behaviour +as much as they are able. Right down to the use of JSON formatted output +to facilitate the use of their API without having to retrain developers. + +This API does not do that. It would not be able to do that and also +provide access to the entire C API on which it\'s built. It does, +however, provide a very pythonic interface on top of the direct bindings +and it\'s this pythonic layer that this HOWTO deals with. + +.. _howto-get-context: + +Context +------- + +One of the reasons which prevents this API from being RESTful is that +most operations require more than one instruction to the API to perform +the task. Sure, there are certain functions which can be performed +simultaneously, particularly if the result known or strongly anticipated +(e.g. selecting and encrypting to a key known to be in the public +keybox). + +There are many more, however, which cannot be manipulated so readily: +they must be performed in a specific sequence and the result of one +operation has a direct bearing on the outcome of subsequent operations. +Not merely by generating an error either. + +When dealing with this type of persistent state on the web, full of both +the RESTful and REST-like, it\'s most commonly referred to as a session. +In GPGME, however, it is called a context and every operation type has +one. + +.. _howto-keys: + +Working with keys +================= + +.. _howto-keys-selection: + +Key selection +------------- + +Selecting keys to encrypt to or to sign with will be a common occurrence +when working with GPGMe and the means available for doing so are quite +simple. + +They do depend on utilising a Context; however once the data is recorded +in another variable, that Context does not need to be the same one which +subsequent operations are performed. + +The easiest way to select a specific key is by searching for that key\'s +key ID or fingerprint, preferably the full fingerprint without any +spaces in it. A long key ID will probably be okay, but is not advised +and short key IDs are already a problem with some being generated to +match specific patterns. It does not matter whether the pattern is upper +or lower case. + +So this is the best method: + +.. code:: python + + import gpg + + k = gpg.Context().keylist(pattern="258E88DCBD3CD44D8E7AB43F6ECB6AF0DEADBEEF") + keys = list(k) + +This is passable and very likely to be common: + +.. code:: python + + import gpg + + k = gpg.Context().keylist(pattern="0x6ECB6AF0DEADBEEF") + keys = list(k) + +And this is a really bad idea: + +.. code:: python + + import gpg + + k = gpg.Context().keylist(pattern="0xDEADBEEF") + keys = list(k) + +Alternatively it may be that the intention is to create a list of keys +which all match a particular search string. For instance all the +addresses at a particular domain, like this: + +.. code:: python + + import gpg + + ncsc = gpg.Context().keylist(pattern="ncsc.mil") + nsa = list(ncsc) + +.. _howto-keys-counting: + +Counting keys +~~~~~~~~~~~~~ + +Counting the number of keys in your public keybox (``pubring.kbx``), the +format which has superseded the old keyring format (``pubring.gpg`` and +``secring.gpg``), or the number of secret keys is a very simple task. + +.. code:: python + + import gpg + + c = gpg.Context() + seckeys = c.keylist(pattern=None, secret=True) + pubkeys = c.keylist(pattern=None, secret=False) + + seclist = list(seckeys) + secnum = len(seclist) + + publist = list(pubkeys) + pubnum = len(publist) + + print(""" + Number of secret keys: {0} + Number of public keys: {1} + """.format(secnum, pubnum)) + +NOTE: The `Cython <#cython>`__ introduction in the `Advanced and +Experimental <#advanced-use>`__ section uses this same key counting code +with Cython to demonstrate some areas where Cython can improve +performance even with the bindings. Users with large public keyrings or +keyboxes, for instance, should consider these options if they are +comfortable with using Cython. + +.. _howto-get-key: + +Get key +------- + +An alternative method of getting a single key via its fingerprint is +available directly within a Context with ``Context().get_key``. This is +the preferred method of selecting a key in order to modify it, sign or +certify it and for obtaining relevant data about a single key as a part +of other functions; when verifying a signature made by that key, for +instance. + +By default this method will select public keys, but it can select secret +keys as well. + +This first example demonstrates selecting the current key of Werner +Koch, which is due to expire at the end of 2018: + +.. code:: python + + import gpg + + fingerprint = "80615870F5BAD690333686D0F2AD85AC1E42B367" + key = gpg.Context().get_key(fingerprint) + +Whereas this example demonstrates selecting the author\'s current key +with the ``secret`` key word argument set to ``True``: + +.. code:: python + + import gpg + + fingerprint = "DB4724E6FA4286C92B4E55C4321E4E2373590E5D" + key = gpg.Context().get_key(fingerprint, secret=True) + +It is, of course, quite possible to select expired, disabled and revoked +keys with this function, but only to effectively display information +about those keys. + +It is also possible to use both unicode or string literals and byte +literals with the fingerprint when getting a key in this way. + +.. _howto-import-key: + +Importing keys +-------------- + +Importing keys is possible with the ``key_import()`` method and takes +one argument which is a bytes literal object containing either the +binary or ASCII armoured key data for one or more keys. + +The following example retrieves one or more keys from the SKS keyservers +via the web using the requests module. Since requests returns the +content as a bytes literal object, we can then use that directly to +import the resulting data into our keybox. + +.. code:: python + + import gpg + import os.path + import requests + + c = gpg.Context() + url = "https://sks-keyservers.net/pks/lookup" + pattern = input("Enter the pattern to search for key or user IDs: ") + payload = {"op": "get", "search": pattern} + + r = requests.get(url, verify=True, params=payload) + result = c.key_import(r.content) + + if result is not None and hasattr(result, "considered") is False: + print(result) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" + The total number of keys considered for import was: {0} + + Number of keys revoked: {1} + Number of new signatures: {2} + Number of new subkeys: {3} + Number of new user IDs: {4} + Number of new secret keys: {5} + Number of unchanged keys: {6} + + The key IDs for all considered keys were: + """.format(num_keys, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print("{0}\n".format(result.imports[i].fpr)) + else: + pass + +NOTE: When searching for a key ID of any length or a fingerprint +(without spaces), the SKS servers require the the leading ``0x`` +indicative of hexadecimal be included. Also note that the old short key +IDs (e.g. ``0xDEADBEEF``) should no longer be used due to the relative +ease by which such key IDs can be reproduced, as demonstrated by the +Evil32 Project in 2014 (which was subsequently exploited in 2016). + +.. _import-protonmail: + +Working with ProtonMail +~~~~~~~~~~~~~~~~~~~~~~~ + +Here is a variation on the example above which checks the constrained +ProtonMail keyserver for ProtonMail public keys. + +.. code:: python + + import gpg + import requests + import sys + + print(""" + This script searches the ProtonMail key server for the specified key and + imports it. + """) + + c = gpg.Context(armor=True) + url = "https://api.protonmail.ch/pks/lookup" + ksearch = [] + + if len(sys.argv) >= 2: + keyterm = sys.argv[1] + else: + keyterm = input("Enter the key ID, UID or search string: ") + + if keyterm.count("@") == 2 and keyterm.startswith("@") is True: + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + elif keyterm.count("@") == 1 and keyterm.startswith("@") is True: + ksearch.append("{0}@protonmail.com".format(keyterm[1:])) + ksearch.append("{0}@protonmail.ch".format(keyterm[1:])) + ksearch.append("{0}@pm.me".format(keyterm[1:])) + elif keyterm.count("@") == 0: + ksearch.append("{0}@protonmail.com".format(keyterm)) + ksearch.append("{0}@protonmail.ch".format(keyterm)) + ksearch.append("{0}@pm.me".format(keyterm)) + elif keyterm.count("@") == 2 and keyterm.startswith("@") is False: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) + elif keyterm.count("@") > 2: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) + else: + ksearch.append(keyterm) + + for k in ksearch: + payload = {"op": "get", "search": k} + try: + r = requests.get(url, verify=True, params=payload) + if r.ok is True: + result = c.key_import(r.content) + elif r.ok is False: + result = r.content + except Exception as e: + result = None + + if result is not None and hasattr(result, "considered") is False: + print("{0} for {1}".format(result.decode(), k)) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" + The total number of keys considered for import was: {0} + + With UIDs wholely or partially matching the following string: + + {1} + + Number of keys revoked: {2} + Number of new signatures: {3} + Number of new subkeys: {4} + Number of new user IDs: {5} + Number of new secret keys: {6} + Number of unchanged keys: {7} + + The key IDs for all considered keys were: + """.format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + elif result is None: + print(e) + +Both the above example, +`pmkey-import.py <../examples/howto/pmkey-import.py>`__, and a version +which prompts for an alternative GnuPG home directory, +`pmkey-import-alt.py <../examples/howto/pmkey-import-alt.py>`__, are +available with the other examples and are executable scripts. + +Note that while the ProtonMail servers are based on the SKS servers, +their server is related more to their API and is not feature complete by +comparison to the servers in the SKS pool. One notable difference being +that the ProtonMail server does not permit non ProtonMail users to +update their own keys, which could be a vector for attacking ProtonMail +users who may not receive a key\'s revocation if it had been +compromised. + +.. _import-hkp4py: + +Importing with HKP for Python +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Performing the same tasks with the `hkp4py +module <https://github.com/Selfnet/hkp4py>`__ (available via PyPI) is +not too much different, but does provide a number of options of benefit +to end users. Not least of which being the ability to perform some +checks on a key before importing it or not. For instance it may be the +policy of a site or project to only import keys which have not been +revoked. The hkp4py module permits such checks prior to the importing of +the keys found. + +.. code:: python + + import gpg + import hkp4py + import sys + + c = gpg.Context() + server = hkp4py.KeyServer("hkps://hkps.pool.sks-keyservers.net") + results = [] + + if len(sys.argv) > 2: + pattern = " ".join(sys.argv[1:]) + elif len(sys.argv) == 2: + pattern = sys.argv[1] + else: + pattern = input("Enter the pattern to search for keys or user IDs: ") + + try: + keys = server.search(pattern) + print("Found {0} key(s).".format(len(keys))) + except Exception as e: + keys = [] + for logrus in pattern.split(): + if logrus.startswith("0x") is True: + key = server.search(logrus) + else: + key = server.search("0x{0}".format(logrus)) + keys.append(key[0]) + print("Found {0} key(s).".format(len(keys))) + + for key in keys: + import_result = c.key_import(key.key_blob) + results.append(import_result) + + for result in results: + if result is not None and hasattr(result, "considered") is False: + print(result) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" + The total number of keys considered for import was: {0} + + Number of keys revoked: {1} + Number of new signatures: {2} + Number of new subkeys: {3} + Number of new user IDs: {4} + Number of new secret keys: {5} + Number of unchanged keys: {6} + + The key IDs for all considered keys were: + """.format(num_keys, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + else: + pass + +Since the hkp4py module handles multiple keys just as effectively as one +(``keys`` is a list of responses per matching key), the example above is +able to do a little bit more with the returned data before anything is +actually imported. + +.. _import-protonmail-hkp4py: + +Importing from ProtonMail with HKP for Python +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Though this can provide certain benefits even when working with +ProtonMail, the scope is somewhat constrained there due to the +limitations of the ProtonMail keyserver. + +For instance, searching the SKS keyserver pool for the term \"gnupg\" +produces hundreds of results from any time the word appears in any part +of a user ID. Performing the same search on the ProtonMail keyserver +returns zero results, even though there are at least two test accounts +which include it as part of the username. + +The cause of this discrepancy is the deliberate configuration of that +server by ProtonMail to require an exact match of the full email address +of the ProtonMail user whose key is being requested. Presumably this is +intended to reduce breaches of privacy of their users as an email +address must already be known before a key for that address can be +obtained. + +#. Import from ProtonMail via HKP for Python Example no. 1 + + The following script is avalable with the rest of the examples under + the somewhat less than original name, ``pmkey-import-hkp.py``. + + .. code:: python + + import gpg + import hkp4py + import os.path + import sys + + print(""" + This script searches the ProtonMail key server for the specified key and + imports it. + + Usage: pmkey-import-hkp.py [search strings] + """) + + c = gpg.Context(armor=True) + server = hkp4py.KeyServer("hkps://api.protonmail.ch") + keyterms = [] + ksearch = [] + allkeys = [] + results = [] + paradox = [] + homeless = None + + if len(sys.argv) > 2: + keyterms = sys.argv[1:] + elif len(sys.argv) == 2: + keyterm = sys.argv[1] + keyterms.append(keyterm) + else: + key_term = input("Enter the key ID, UID or search string: ") + keyterms = key_term.split() + + for keyterm in keyterms: + if keyterm.count("@") == 2 and keyterm.startswith("@") is True: + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + elif keyterm.count("@") == 1 and keyterm.startswith("@") is True: + ksearch.append("{0}@protonmail.com".format(keyterm[1:])) + ksearch.append("{0}@protonmail.ch".format(keyterm[1:])) + ksearch.append("{0}@pm.me".format(keyterm[1:])) + elif keyterm.count("@") == 0: + ksearch.append("{0}@protonmail.com".format(keyterm)) + ksearch.append("{0}@protonmail.ch".format(keyterm)) + ksearch.append("{0}@pm.me".format(keyterm)) + elif keyterm.count("@") == 2 and keyterm.startswith("@") is False: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) + elif keyterm.count("@") > 2: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) + else: + ksearch.append(keyterm) + + for k in ksearch: + print("Checking for key for: {0}".format(k)) + try: + keys = server.search(k) + if isinstance(keys, list) is True: + for key in keys: + allkeys.append(key) + try: + import_result = c.key_import(key.key_blob) + except Exception as e: + import_result = c.key_import(key.key) + else: + paradox.append(keys) + import_result = None + except Exception as e: + import_result = None + results.append(import_result) + + for result in results: + if result is not None and hasattr(result, "considered") is False: + print("{0} for {1}".format(result.decode(), k)) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" + The total number of keys considered for import was: {0} + + With UIDs wholely or partially matching the following string: + + {1} + + Number of keys revoked: {2} + Number of new signatures: {3} + Number of new subkeys: {4} + Number of new user IDs: {5} + Number of new secret keys: {6} + Number of unchanged keys: {7} + + The key IDs for all considered keys were: + """.format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + elif result is None: + pass + +#. Import from ProtonMail via HKP for Python Example no. 2 + + Like its counterpart above, this script can also be found with the + rest of the examples, by the name pmkey-import-hkp-alt.py. + + With this script a modicum of effort has been made to treat anything + passed as a ``homedir`` which either does not exist or which is not a + directory, as also being a pssible user ID to check for. It\'s not + guaranteed to pick up on all such cases, but it should cover most of + them. + + .. code:: python + + import gpg + import hkp4py + import os.path + import sys + + print(""" + This script searches the ProtonMail key server for the specified key and + imports it. Optionally enables specifying a different GnuPG home directory. + + Usage: pmkey-import-hkp.py [homedir] [search string] + or: pmkey-import-hkp.py [search string] + """) + + c = gpg.Context(armor=True) + server = hkp4py.KeyServer("hkps://api.protonmail.ch") + keyterms = [] + ksearch = [] + allkeys = [] + results = [] + paradox = [] + homeless = None + + if len(sys.argv) > 3: + homedir = sys.argv[1] + keyterms = sys.argv[2:] + elif len(sys.argv) == 3: + homedir = sys.argv[1] + keyterm = sys.argv[2] + keyterms.append(keyterm) + elif len(sys.argv) == 2: + homedir = "" + keyterm = sys.argv[1] + keyterms.append(keyterm) + else: + keyterm = input("Enter the key ID, UID or search string: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + keyterms.append(keyterm) + + if len(homedir) == 0: + homedir = None + homeless = False + + if homedir is not None: + if homedir.startswith("~"): + if os.path.exists(os.path.expanduser(homedir)) is True: + if os.path.isdir(os.path.expanduser(homedir)) is True: + c.home_dir = os.path.realpath(os.path.expanduser(homedir)) + else: + homeless = True + else: + homeless = True + elif os.path.exists(os.path.realpath(homedir)) is True: + if os.path.isdir(os.path.realpath(homedir)) is True: + c.home_dir = os.path.realpath(homedir) + else: + homeless = True + else: + homeless = True + + # First check to see if the homedir really is a homedir and if not, treat it as + # a search string. + if homeless is True: + keyterms.append(homedir) + c.home_dir = None + else: + pass + + for keyterm in keyterms: + if keyterm.count("@") == 2 and keyterm.startswith("@") is True: + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + elif keyterm.count("@") == 1 and keyterm.startswith("@") is True: + ksearch.append("{0}@protonmail.com".format(keyterm[1:])) + ksearch.append("{0}@protonmail.ch".format(keyterm[1:])) + ksearch.append("{0}@pm.me".format(keyterm[1:])) + elif keyterm.count("@") == 0: + ksearch.append("{0}@protonmail.com".format(keyterm)) + ksearch.append("{0}@protonmail.ch".format(keyterm)) + ksearch.append("{0}@pm.me".format(keyterm)) + elif keyterm.count("@") == 2 and keyterm.startswith("@") is False: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) + elif keyterm.count("@") > 2: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) + else: + ksearch.append(keyterm) + + for k in ksearch: + print("Checking for key for: {0}".format(k)) + try: + keys = server.search(k) + if isinstance(keys, list) is True: + for key in keys: + allkeys.append(key) + try: + import_result = c.key_import(key.key_blob) + except Exception as e: + import_result = c.key_import(key.key) + else: + paradox.append(keys) + import_result = None + except Exception as e: + import_result = None + results.append(import_result) + + for result in results: + if result is not None and hasattr(result, "considered") is False: + print("{0} for {1}".format(result.decode(), k)) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" + The total number of keys considered for import was: {0} + + With UIDs wholely or partially matching the following string: + + {1} + + Number of keys revoked: {2} + Number of new signatures: {3} + Number of new subkeys: {4} + Number of new user IDs: {5} + Number of new secret keys: {6} + Number of unchanged keys: {7} + + The key IDs for all considered keys were: + """.format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + elif result is None: + pass + +.. _howto-export-key: + +Exporting keys +-------------- + +Exporting keys remains a reasonably simple task, but has been separated +into three different functions for the OpenPGP cryptographic engine. Two +of those functions are for exporting public keys and the third is for +exporting secret keys. + +.. _howto-export-public-key: + +Exporting public keys +~~~~~~~~~~~~~~~~~~~~~ + +There are two methods of exporting public keys, both of which are very +similar to the other. The default method, ``key_export()``, will export +a public key or keys matching a specified pattern as normal. The +alternative, the ``key_export_minimal()`` method, will do the same thing +except producing a minimised output with extra signatures and third +party signatures or certifications removed. + +.. code:: python + + import gpg + import os.path + import sys + + print(""" + This script exports one or more public keys. + """) + + c = gpg.Context(armor=True) + + if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] + elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") + elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + else: + keyfile = input("Enter the path and filename to save the secret key to: ") + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + + if homedir.startswith("~"): + if os.path.exists(os.path.expanduser(homedir)) is True: + c.home_dir = os.path.expanduser(homedir) + else: + pass + elif os.path.exists(homedir) is True: + c.home_dir = homedir + else: + pass + + try: + result = c.key_export(pattern=logrus) + except: + result = c.key_export(pattern=None) + + if result is not None: + with open(keyfile, "wb") as f: + f.write(result) + else: + pass + +It should be noted that the result will only return ``None`` when a +search pattern has been entered, but has not matched any keys. When the +search pattern itself is set to ``None`` this triggers the exporting of +the entire public keybox. + +.. code:: python + + import gpg + import os.path + import sys + + print(""" + This script exports one or more public keys in minimised form. + """) + + c = gpg.Context(armor=True) + + if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] + elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") + elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + else: + keyfile = input("Enter the path and filename to save the secret key to: ") + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + + if homedir.startswith("~"): + if os.path.exists(os.path.expanduser(homedir)) is True: + c.home_dir = os.path.expanduser(homedir) + else: + pass + elif os.path.exists(homedir) is True: + c.home_dir = homedir + else: + pass + + try: + result = c.key_export_minimal(pattern=logrus) + except: + result = c.key_export_minimal(pattern=None) + + if result is not None: + with open(keyfile, "wb") as f: + f.write(result) + else: + pass + +.. _howto-export-secret-key: + +Exporting secret keys +~~~~~~~~~~~~~~~~~~~~~ + +Exporting secret keys is, functionally, very similar to exporting public +keys; save for the invocation of ``pinentry`` via ``gpg-agent`` in order +to securely enter the key\'s passphrase and authorise the export. + +The following example exports the secret key to a file which is then set +with the same permissions as the output files created by the command +line secret key export options. + +.. code:: python + + import gpg + import os + import os.path + import sys + + print(""" + This script exports one or more secret keys. + + The gpg-agent and pinentry are invoked to authorise the export. + """) + + c = gpg.Context(armor=True) + + if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] + elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") + elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + else: + keyfile = input("Enter the path and filename to save the secret key to: ") + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + + if len(homedir) == 0: + homedir = None + elif homedir.startswith("~"): + userdir = os.path.expanduser(homedir) + if os.path.exists(userdir) is True: + homedir = os.path.realpath(userdir) + else: + homedir = None + else: + homedir = os.path.realpath(homedir) + + if os.path.exists(homedir) is False: + homedir = None + else: + if os.path.isdir(homedir) is False: + homedir = None + else: + pass + + if homedir is not None: + c.home_dir = homedir + else: + pass + + try: + result = c.key_export_secret(pattern=logrus) + except: + result = c.key_export_secret(pattern=None) + + if result is not None: + with open(keyfile, "wb") as f: + f.write(result) + os.chmod(keyfile, 0o600) + else: + pass + +Alternatively the approach of the following script can be used. This +longer example saves the exported secret key(s) in files in the GnuPG +home directory, in addition to setting the file permissions as only +readable and writable by the user. It also exports the secret key(s) +twice in order to output both GPG binary (``.gpg``) and ASCII armoured +(``.asc``) files. + +.. code:: python + + import gpg + import os + import os.path + import subprocess + import sys + + print(""" + This script exports one or more secret keys as both ASCII armored and binary + file formats, saved in files within the user's GPG home directory. + + The gpg-agent and pinentry are invoked to authorise the export. + """) + + if sys.platform == "win32": + gpgconfcmd = "gpgconf.exe --list-dirs homedir" + else: + gpgconfcmd = "gpgconf --list-dirs homedir" + + a = gpg.Context(armor=True) + b = gpg.Context() + c = gpg.Context() + + if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] + elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") + elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + else: + keyfile = input("Enter the filename to save the secret key to: ") + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + + if len(homedir) == 0: + homedir = None + elif homedir.startswith("~"): + userdir = os.path.expanduser(homedir) + if os.path.exists(userdir) is True: + homedir = os.path.realpath(userdir) + else: + homedir = None + else: + homedir = os.path.realpath(homedir) + + if os.path.exists(homedir) is False: + homedir = None + else: + if os.path.isdir(homedir) is False: + homedir = None + else: + pass + + if homedir is not None: + c.home_dir = homedir + else: + pass + + if c.home_dir is not None: + if c.home_dir.endswith("/"): + gpgfile = "{0}{1}.gpg".format(c.home_dir, keyfile) + ascfile = "{0}{1}.asc".format(c.home_dir, keyfile) + else: + gpgfile = "{0}/{1}.gpg".format(c.home_dir, keyfile) + ascfile = "{0}/{1}.asc".format(c.home_dir, keyfile) + else: + if os.path.exists(os.environ["GNUPGHOME"]) is True: + hd = os.environ["GNUPGHOME"] + else: + try: + hd = subprocess.getoutput(gpgconfcmd) + except: + process = subprocess.Popen(gpgconfcmd.split(), + stdout=subprocess.PIPE) + procom = process.communicate() + if sys.version_info[0] == 2: + hd = procom[0].strip() + else: + hd = procom[0].decode().strip() + gpgfile = "{0}/{1}.gpg".format(hd, keyfile) + ascfile = "{0}/{1}.asc".format(hd, keyfile) + + try: + a_result = a.key_export_secret(pattern=logrus) + b_result = b.key_export_secret(pattern=logrus) + except: + a_result = a.key_export_secret(pattern=None) + b_result = b.key_export_secret(pattern=None) + + if a_result is not None: + with open(ascfile, "wb") as f: + f.write(a_result) + os.chmod(ascfile, 0o600) + else: + pass + + if b_result is not None: + with open(gpgfile, "wb") as f: + f.write(b_result) + os.chmod(gpgfile, 0o600) + else: + pass + +.. _howto-send-public-key: + +Sending public keys to the SKS Keyservers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As with the previous section on importing keys, the ``hkp4py`` module +adds another option with exporting keys in order to send them to the +public keyservers. + +The following example demonstrates how this may be done. + +.. code:: python + + import gpg + import hkp4py + import os.path + import sys + + print(""" + This script sends one or more public keys to the SKS keyservers and is + essentially a slight variation on the export-key.py script. + """) + + c = gpg.Context(armor=True) + server = hkp4py.KeyServer("hkps://hkps.pool.sks-keyservers.net") + + if len(sys.argv) > 2: + logrus = " ".join(sys.argv[1:]) + elif len(sys.argv) == 2: + logrus = sys.argv[1] + else: + logrus = input("Enter the UID matching the key(s) to send: ") + + if len(logrus) > 0: + try: + export_result = c.key_export(pattern=logrus) + except Exception as e: + print(e) + export_result = None + else: + export_result = c.key_export(pattern=None) + + if export_result is not None: + try: + try: + send_result = server.add(export_result) + except: + send_result = server.add(export_result.decode()) + if send_result is not None: + print(send_result) + else: + pass + except Exception as e: + print(e) + else: + pass + +An expanded version of this script with additional functions for +specifying an alternative homedir location is in the examples directory +as ``send-key-to-keyserver.py``. + +The ``hkp4py`` module appears to handle both string and byte literal +text data equally well, but the GPGME bindings deal primarily with byte +literal data only and so this script sends in that format first, then +tries the string literal form. + +.. _howto-the-basics: + +Basic Functions +=============== + +The most frequently called features of any cryptographic library will be +the most fundamental tasks for encryption software. In this section we +will look at how to programmatically encrypt data, decrypt it, sign it +and verify signatures. + +.. _howto-basic-encryption: + +Encryption +---------- + +Encrypting is very straight forward. In the first example below the +message, ``text``, is encrypted to a single recipient\'s key. In the +second example the message will be encrypted to multiple recipients. + +.. _howto-basic-encryption-single: + +Encrypting to one key +~~~~~~~~~~~~~~~~~~~~~ + +Once the the Context is set the main issues with encrypting data is +essentially reduced to key selection and the keyword arguments specified +in the ``gpg.Context().encrypt()`` method. + +Those keyword arguments are: ``recipients``, a list of keys encrypted to +(covered in greater detail in the following section); ``sign``, whether +or not to sign the plaintext data, see subsequent sections on signing +and verifying signatures below (defaults to ``True``); ``sink``, to +write results or partial results to a secure sink instead of returning +it (defaults to ``None``); ``passphrase``, only used when utilising +symmetric encryption (defaults to ``None``); ``always_trust``, used to +override the trust model settings for recipient keys (defaults to +``False``); ``add_encrypt_to``, utilises any preconfigured +``encrypt-to`` or ``default-key`` settings in the user\'s ``gpg.conf`` +file (defaults to ``False``); ``prepare``, prepare for encryption +(defaults to ``False``); ``expect_sign``, prepare for signing (defaults +to ``False``); ``compress``, compresses the plaintext prior to +encryption (defaults to ``True``). + +.. code:: python + + import gpg + + a_key = "0x12345678DEADBEEF" + text = b"""Some text to test with. + + Since the text in this case must be bytes, it is most likely that + the input form will be a separate file which is opened with "rb" + as this is the simplest method of obtaining the correct data format. + """ + + c = gpg.Context(armor=True) + rkey = list(c.keylist(pattern=a_key, secret=False)) + ciphertext, result, sign_result = c.encrypt(text, recipients=rkey, sign=False) + + with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) + +Though this is even more likely to be used like this; with the plaintext +input read from a file, the recipient keys used for encryption +regardless of key trust status and the encrypted output also encrypted +to any preconfigured keys set in the ``gpg.conf`` file: + +.. code:: python + + import gpg + + a_key = "0x12345678DEADBEEF" + + with open("secret_plans.txt", "rb") as afile: + text = afile.read() + + c = gpg.Context(armor=True) + rkey = list(c.keylist(pattern=a_key, secret=False)) + ciphertext, result, sign_result = c.encrypt(text, recipients=rkey, sign=True, + always_trust=True, + add_encrypt_to=True) + + with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) + +If the ``recipients`` paramater is empty then the plaintext is encrypted +symmetrically. If no ``passphrase`` is supplied as a parameter or via a +callback registered with the ``Context()`` then an out-of-band prompt +for the passphrase via pinentry will be invoked. + +.. _howto-basic-encryption-multiple: + +Encrypting to multiple keys +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Encrypting to multiple keys essentially just expands upon the key +selection process and the recipients from the previous examples. + +The following example encrypts a message (``text``) to everyone with an +email address on the ``gnupg.org`` domain, [4]_ but does *not* encrypt +to a default key or other key which is configured to normally encrypt +to. + +.. code:: python + + import gpg + + text = b"""Oh look, another test message. + + The same rules apply as with the previous example and more likely + than not, the message will actually be drawn from reading the + contents of a file or, maybe, from entering data at an input() + prompt. + + Since the text in this case must be bytes, it is most likely that + the input form will be a separate file which is opened with "rb" + as this is the simplest method of obtaining the correct data + format. + """ + + c = gpg.Context(armor=True) + rpattern = list(c.keylist(pattern="@gnupg.org", secret=False)) + logrus = [] + + for i in range(len(rpattern)): + if rpattern[i].can_encrypt == 1: + logrus.append(rpattern[i]) + + ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, + sign=False, always_trust=True) + + with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) + +All it would take to change the above example to sign the message and +also encrypt the message to any configured default keys would be to +change the ``c.encrypt`` line to this: + +.. code:: python + + ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, + always_trust=True, + add_encrypt_to=True) + +The only keyword arguments requiring modification are those for which +the default values are changing. The default value of ``sign`` is +``True``, the default of ``always_trust`` is ``False``, the default of +``add_encrypt_to`` is ``False``. + +If ``always_trust`` is not set to ``True`` and any of the recipient keys +are not trusted (e.g. not signed or locally signed) then the encryption +will raise an error. It is possible to mitigate this somewhat with +something more like this: + +.. code:: python + + import gpg + + with open("secret_plans.txt.asc", "rb") as afile: + text = afile.read() + + c = gpg.Context(armor=True) + rpattern = list(c.keylist(pattern="@gnupg.org", secret=False)) + logrus = [] + + for i in range(len(rpattern)): + if rpattern[i].can_encrypt == 1: + logrus.append(rpattern[i]) + + try: + ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, + add_encrypt_to=True) + except gpg.errors.InvalidRecipients as e: + for i in range(len(e.recipients)): + for n in range(len(logrus)): + if logrus[n].fpr == e.recipients[i].fpr: + logrus.remove(logrus[n]) + else: + pass + try: + ciphertext, result, sign_result = c.encrypt(text, + recipients=logrus, + add_encrypt_to=True) + with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) + except: + pass + +This will attempt to encrypt to all the keys searched for, then remove +invalid recipients if it fails and try again. + +.. _howto-basic-decryption: + +Decryption +---------- + +Decrypting something encrypted to a key in one\'s secret keyring is +fairly straight forward. + +In this example code, however, preconfiguring either ``gpg.Context()`` +or ``gpg.core.Context()`` as ``c`` is unnecessary because there is no +need to modify the Context prior to conducting the decryption and since +the Context is only used once, setting it to ``c`` simply adds lines for +no gain. + +.. code:: python + + import gpg + + ciphertext = input("Enter path and filename of encrypted file: ") + newfile = input("Enter path and filename of file to save decrypted data to: ") + + with open(ciphertext, "rb") as cfile: + try: + plaintext, result, verify_result = gpg.Context().decrypt(cfile) + except gpg.errors.GPGMEError as e: + plaintext = None + print(e) + + if plaintext is not None: + with open(newfile, "wb") as nfile: + nfile.write(plaintext) + else: + pass + +The data available in ``plaintext`` in this example is the decrypted +content as a byte object, the recipient key IDs and algorithms in +``result`` and the results of verifying any signatures of the data in +``verify_result``. + +.. _howto-basic-signing: + +Signing text and files +---------------------- + +The following sections demonstrate how to specify keys to sign with. + +.. _howto-basic-signing-signers: + +Signing key selection +~~~~~~~~~~~~~~~~~~~~~ + +By default GPGME and the Python bindings will use the default key +configured for the user invoking the GPGME API. If there is no default +key specified and there is more than one secret key available it may be +necessary to specify the key or keys with which to sign messages and +files. + +.. code:: python + + import gpg + + logrus = input("Enter the email address or string to match signing keys to: ") + hancock = gpg.Context().keylist(pattern=logrus, secret=True) + sig_src = list(hancock) + +The signing examples in the following sections include the explicitly +designated ``signers`` parameter in two of the five examples; once where +the resulting signature would be ASCII armoured and once where it would +not be armoured. + +While it would be possible to enter a key ID or fingerprint here to +match a specific key, it is not possible to enter two fingerprints and +match two keys since the patten expects a string, bytes or None and not +a list. A string with two fingerprints won\'t match any single key. + +.. _howto-basic-signing-normal: + +Normal or default signing messages or files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The normal or default signing process is essentially the same as is most +often invoked when also encrypting a message or file. So when the +encryption component is not utilised, the result is to produce an +encoded and signed output which may or may not be ASCII armoured and +which may or may not also be compressed. + +By default compression will be used unless GnuPG detects that the +plaintext is already compressed. ASCII armouring will be determined +according to the value of ``gpg.Context().armor``. + +The compression algorithm is selected in much the same way as the +symmetric encryption algorithm or the hash digest algorithm is when +multiple keys are involved; from the preferences saved into the key +itself or by comparison with the preferences with all other keys +involved. + +.. code:: python + + import gpg + + text0 = """Declaration of ... something. + + """ + text = text0.encode() + + c = gpg.Context(armor=True, signers=sig_src) + signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.NORMAL) + + with open("/path/to/statement.txt.asc", "w") as afile: + afile.write(signed_data.decode()) + +Though everything in this example is accurate, it is more likely that +reading the input data from another file and writing the result to a new +file will be performed more like the way it is done in the next example. +Even if the output format is ASCII armoured. + +.. code:: python + + import gpg + + with open("/path/to/statement.txt", "rb") as tfile: + text = tfile.read() + + c = gpg.Context() + signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.NORMAL) + + with open("/path/to/statement.txt.sig", "wb") as afile: + afile.write(signed_data) + +.. _howto-basic-signing-detached: + +Detached signing messages and files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Detached signatures will often be needed in programmatic uses of GPGME, +either for signing files (e.g. tarballs of code releases) or as a +component of message signing (e.g. PGP/MIME encoded email). + +.. code:: python + + import gpg + + text0 = """Declaration of ... something. + + """ + text = text0.encode() + + c = gpg.Context(armor=True) + signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.DETACH) + + with open("/path/to/statement.txt.asc", "w") as afile: + afile.write(signed_data.decode()) + +As with normal signatures, detached signatures are best handled as byte +literals, even when the output is ASCII armoured. + +.. code:: python + + import gpg + + with open("/path/to/statement.txt", "rb") as tfile: + text = tfile.read() + + c = gpg.Context(signers=sig_src) + signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.DETACH) + + with open("/path/to/statement.txt.sig", "wb") as afile: + afile.write(signed_data) + +.. _howto-basic-signing-clear: + +Clearsigning messages or text +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Though PGP/in-line messages are no longer encouraged in favour of +PGP/MIME, there is still sometimes value in utilising in-line +signatures. This is where clear-signed messages or text is of value. + +.. code:: python + + import gpg + + text0 = """Declaration of ... something. + + """ + text = text0.encode() + + c = gpg.Context() + signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.CLEAR) + + with open("/path/to/statement.txt.asc", "w") as afile: + afile.write(signed_data.decode()) + +In spite of the appearance of a clear-signed message, the data handled +by GPGME in signing it must still be byte literals. + +.. code:: python + + import gpg + + with open("/path/to/statement.txt", "rb") as tfile: + text = tfile.read() + + c = gpg.Context() + signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.CLEAR) + + with open("/path/to/statement.txt.asc", "wb") as afile: + afile.write(signed_data) + +.. _howto-basic-verification: + +Signature verification +---------------------- + +Essentially there are two principal methods of verification of a +signature. The first of these is for use with the normal or default +signing method and for clear-signed messages. The second is for use with +files and data with detached signatures. + +The following example is intended for use with the default signing +method where the file was not ASCII armoured: + +.. code:: python + + import gpg + import time + + filename = "statement.txt" + gpg_file = "statement.txt.gpg" + + c = gpg.Context() + + try: + data, result = c.verify(open(gpg_file)) + verified = True + except gpg.errors.BadSignatures as e: + verified = False + print(e) + + if verified is True: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: + {0} + with key {1} + made at {2} + """.format(c.get_key(sign.fpr).uids[0].uid, sign.fpr, + time.ctime(sign.timestamp))) + else: + pass + +Whereas this next example, which is almost identical would work with +normal ASCII armoured files and with clear-signed files: + +.. code:: python + + import gpg + import time + + filename = "statement.txt" + asc_file = "statement.txt.asc" + + c = gpg.Context() + + try: + data, result = c.verify(open(asc_file)) + verified = True + except gpg.errors.BadSignatures as e: + verified = False + print(e) + + if verified is True: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: + {0} + with key {1} + made at {2} + """.format(c.get_key(sign.fpr).uids[0].uid, sign.fpr, + time.ctime(sign.timestamp))) + else: + pass + +In both of the previous examples it is also possible to compare the +original data that was signed against the signed data in ``data`` to see +if it matches with something like this: + +.. code:: python + + with open(filename, "rb") as afile: + text = afile.read() + + if text == data: + print("Good signature.") + else: + pass + +The following two examples, however, deal with detached signatures. With +his method of verification the data that was signed does not get +returned since it is already being explicitly referenced in the first +argument of ``c.verify``. So ``data`` is ``None`` and only the +information in ``result`` is available. + +.. code:: python + + import gpg + import time + + filename = "statement.txt" + sig_file = "statement.txt.sig" + + c = gpg.Context() + + try: + data, result = c.verify(open(filename), open(sig_file)) + verified = True + except gpg.errors.BadSignatures as e: + verified = False + print(e) + + if verified is True: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: + {0} + with key {1} + made at {2} + """.format(c.get_key(sign.fpr).uids[0].uid, sign.fpr, + time.ctime(sign.timestamp))) + else: + pass + +.. code:: python + + import gpg + import time + + filename = "statement.txt" + asc_file = "statement.txt.asc" + + c = gpg.Context() + + try: + data, result = c.verify(open(filename), open(asc_file)) + verified = True + except gpg.errors.BadSignatures as e: + verified = False + print(e) + + if verified is True: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: + {0} + with key {1} + made at {2} + """.format(c.get_key(sign.fpr).uids[0].uid, sign.fpr, + time.ctime(sign.timestamp))) + else: + pass + +.. _key-generation: + +Creating keys and subkeys +========================= + +The one thing, aside from GnuPG itself, that GPGME depends on, of +course, is the keys themselves. So it is necessary to be able to +generate them and modify them by adding subkeys, revoking or disabling +them, sometimes deleting them and doing the same for user IDs. + +In the following examples a key will be created for the world\'s +greatest secret agent, Danger Mouse. Since Danger Mouse is a secret +agent he needs to be able to protect information to ``SECRET`` level +clearance, so his keys will be 3072-bit keys. + +The pre-configured ``gpg.conf`` file which sets cipher, digest and other +preferences contains the following configuration parameters: + +.. code:: conf + + expert + allow-freeform-uid + allow-secret-key-import + trust-model tofu+pgp + tofu-default-policy unknown + enable-large-rsa + enable-dsa2 + cert-digest-algo SHA512 + default-preference-list TWOFISH CAMELLIA256 AES256 CAMELLIA192 AES192 CAMELLIA128 AES BLOWFISH IDEA CAST5 3DES SHA512 SHA384 SHA256 SHA224 RIPEMD160 SHA1 ZLIB BZIP2 ZIP Uncompressed + personal-cipher-preferences TWOFISH CAMELLIA256 AES256 CAMELLIA192 AES192 CAMELLIA128 AES BLOWFISH IDEA CAST5 3DES + personal-digest-preferences SHA512 SHA384 SHA256 SHA224 RIPEMD160 SHA1 + personal-compress-preferences ZLIB BZIP2 ZIP Uncompressed + +.. _keygen-primary: + +Primary key +----------- + +Generating a primary key uses the ``create_key`` method in a Context. It +contains multiple arguments and keyword arguments, including: +``userid``, ``algorithm``, ``expires_in``, ``expires``, ``sign``, +``encrypt``, ``certify``, ``authenticate``, ``passphrase`` and +``force``. The defaults for all of those except ``userid``, +``algorithm``, ``expires_in``, ``expires`` and ``passphrase`` is +``False``. The defaults for ``algorithm`` and ``passphrase`` is +``None``. The default for ``expires_in`` is ``0``. The default for +``expires`` is ``True``. There is no default for ``userid``. + +If ``passphrase`` is left as ``None`` then the key will not be generated +with a passphrase, if ``passphrase`` is set to a string then that will +be the passphrase and if ``passphrase`` is set to ``True`` then +gpg-agent will launch pinentry to prompt for a passphrase. For the sake +of convenience, these examples will keep ``passphrase`` set to ``None``. + +.. code:: python + + import gpg + + c = gpg.Context() + + c.home_dir = "~/.gnupg-dm" + userid = "Danger Mouse <dm@secret.example.net>" + + dmkey = c.create_key(userid, algorithm="rsa3072", expires_in=31536000, + sign=True, certify=True) + +One thing to note here is the use of setting the ``c.home_dir`` +parameter. This enables generating the key or keys in a different +location. In this case to keep the new key data created for this example +in a separate location rather than adding it to existing and active key +store data. As with the default directory, ``~/.gnupg``, any temporary +or separate directory needs the permissions set to only permit access by +the directory owner. On posix systems this means setting the directory +permissions to 700. + +The ``temp-homedir-config.py`` script in the HOWTO examples directory +will create an alternative homedir with these configuration options +already set and the correct directory and file permissions. + +The successful generation of the key can be confirmed via the returned +``GenkeyResult`` object, which includes the following data: + +.. code:: python + + print(""" + Fingerprint: {0} + Primary Key: {1} + Public Key: {2} + Secret Key: {3} + Sub Key: {4} + User IDs: {5} + """.format(dmkey.fpr, dmkey.primary, dmkey.pubkey, dmkey.seckey, dmkey.sub, + dmkey.uid)) + +Alternatively the information can be confirmed using the command line +program: + +.. code:: shell + + bash-4.4$ gpg --homedir ~/.gnupg-dm -K + ~/.gnupg-dm/pubring.kbx + ---------------------- + sec rsa3072 2018-03-15 [SC] [expires: 2019-03-15] + 177B7C25DB99745EE2EE13ED026D2F19E99E63AA + uid [ultimate] Danger Mouse <dm@secret.example.net> + + bash-4.4$ + +As with generating keys manually, to preconfigure expanded preferences +for the cipher, digest and compression algorithms, the ``gpg.conf`` file +must contain those details in the home directory in which the new key is +being generated. I used a cut down version of my own ``gpg.conf`` file +in order to be able to generate this: + +.. code:: shell + + bash-4.4$ gpg --homedir ~/.gnupg-dm --edit-key 177B7C25DB99745EE2EE13ED026D2F19E99E63AA showpref quit + Secret key is available. + + sec rsa3072/026D2F19E99E63AA + created: 2018-03-15 expires: 2019-03-15 usage: SC + trust: ultimate validity: ultimate + [ultimate] (1). Danger Mouse <dm@secret.example.net> + + [ultimate] (1). Danger Mouse <dm@secret.example.net> + Cipher: TWOFISH, CAMELLIA256, AES256, CAMELLIA192, AES192, CAMELLIA128, AES, BLOWFISH, IDEA, CAST5, 3DES + Digest: SHA512, SHA384, SHA256, SHA224, RIPEMD160, SHA1 + Compression: ZLIB, BZIP2, ZIP, Uncompressed + Features: MDC, Keyserver no-modify + + bash-4.4$ + +.. _keygen-subkeys: + +Subkeys +------- + +Adding subkeys to a primary key is fairly similar to creating the +primary key with the ``create_subkey`` method. Most of the arguments are +the same, but not quite all. Instead of the ``userid`` argument there is +now a ``key`` argument for selecting which primary key to add the subkey +to. + +In the following example an encryption subkey will be added to the +primary key. Since Danger Mouse is a security conscious secret agent, +this subkey will only be valid for about six months, half the length of +the primary key. + +.. code:: python + + import gpg + + c = gpg.Context() + c.home_dir = "~/.gnupg-dm" + + key = c.get_key(dmkey.fpr, secret=True) + dmsub = c.create_subkey(key, algorithm="rsa3072", expires_in=15768000, + encrypt=True) + +As with the primary key, the results here can be checked with: + +.. code:: python + + print(""" + Fingerprint: {0} + Primary Key: {1} + Public Key: {2} + Secret Key: {3} + Sub Key: {4} + User IDs: {5} + """.format(dmsub.fpr, dmsub.primary, dmsub.pubkey, dmsub.seckey, dmsub.sub, + dmsub.uid)) + +As well as on the command line with: + +.. code:: shell + + bash-4.4$ gpg --homedir ~/.gnupg-dm -K + ~/.gnupg-dm/pubring.kbx + ---------------------- + sec rsa3072 2018-03-15 [SC] [expires: 2019-03-15] + 177B7C25DB99745EE2EE13ED026D2F19E99E63AA + uid [ultimate] Danger Mouse <dm@secret.example.net> + ssb rsa3072 2018-03-15 [E] [expires: 2018-09-13] + + bash-4.4$ + +.. _keygen-uids: + +User IDs +-------- + +.. _keygen-uids-add: + +Adding User IDs +~~~~~~~~~~~~~~~ + +By comparison to creating primary keys and subkeys, adding a new user ID +to an existing key is much simpler. The method used to do this is +``key_add_uid`` and the only arguments it takes are for the ``key`` and +the new ``uid``. + +.. code:: python + + import gpg + + c = gpg.Context() + c.home_dir = "~/.gnupg-dm" + + dmfpr = "177B7C25DB99745EE2EE13ED026D2F19E99E63AA" + key = c.get_key(dmfpr, secret=True) + uid = "Danger Mouse <danger.mouse@secret.example.net>" + + c.key_add_uid(key, uid) + +Unsurprisingly the result of this is: + +.. code:: shell + + bash-4.4$ gpg --homedir ~/.gnupg-dm -K + ~/.gnupg-dm/pubring.kbx + ---------------------- + sec rsa3072 2018-03-15 [SC] [expires: 2019-03-15] + 177B7C25DB99745EE2EE13ED026D2F19E99E63AA + uid [ultimate] Danger Mouse <danger.mouse@secret.example.net> + uid [ultimate] Danger Mouse <dm@secret.example.net> + ssb rsa3072 2018-03-15 [E] [expires: 2018-09-13] + + bash-4.4$ + +.. _keygen-uids-revoke: + +Revokinging User IDs +~~~~~~~~~~~~~~~~~~~~ + +Revoking a user ID is a fairly similar process, except that it uses the +``key_revoke_uid`` method. + +.. code:: python + + import gpg + + c = gpg.Context() + c.home_dir = "~/.gnupg-dm" + + dmfpr = "177B7C25DB99745EE2EE13ED026D2F19E99E63AA" + key = c.get_key(dmfpr, secret=True) + uid = "Danger Mouse <danger.mouse@secret.example.net>" + + c.key_revoke_uid(key, uid) + +.. _key-sign: + +Key certification +----------------- + +Since key certification is more frequently referred to as key signing, +the method used to perform this function is ``key_sign``. + +The ``key_sign`` method takes four arguments: ``key``, ``uids``, +``expires_in`` and ``local``. The default value of ``uids`` is ``None`` +and which results in all user IDs being selected. The default value of +both ``expires_in`` and ``local`` is ``False``; which results in the +signature never expiring and being able to be exported. + +The ``key`` is the key being signed rather than the key doing the +signing. To change the key doing the signing refer to the signing key +selection above for signing messages and files. + +If the ``uids`` value is not ``None`` then it must either be a string to +match a single user ID or a list of strings to match multiple user IDs. +In this case the matching of those strings must be precise and it is +case sensitive. + +To sign Danger Mouse\'s key for just the initial user ID with a +signature which will last a little over a month, do this: + +.. code:: python + + import gpg + + c = gpg.Context() + uid = "Danger Mouse <dm@secret.example.net>" + + dmfpr = "177B7C25DB99745EE2EE13ED026D2F19E99E63AA" + key = c.get_key(dmfpr, secret=True) + c.key_sign(key, uids=uid, expires_in=2764800) + +.. _advanced-use: + +Advanced or Experimental Use Cases +================================== + +.. _cython: + +C plus Python plus SWIG plus Cython +----------------------------------- + +In spite of the apparent incongruence of using Python bindings to a C +interface only to generate more C from the Python; it is in fact quite +possible to use the GPGME bindings with +`Cython <http://docs.cython.org/en/latest/index.html>`__. Though in many +cases the benefits may not be obvious since the most computationally +intensive work never leaves the level of the C code with which GPGME +itself is interacting with. + +Nevertheless, there are some situations where the benefits are +demonstrable. One of the better and easier examples being the one of the +early examples in this HOWTO, the `key +counting <#howto-keys-counting>`__ code. Running that example as an +executable Python script, ``keycount.py`` (available in the +``examples/howto/`` directory), will take a noticable amount of time to +run on most systems where the public keybox or keyring contains a few +thousand public keys. + +Earlier in the evening, prior to starting this section, I ran that +script on my laptop; as I tend to do periodically and timed it using +``time`` utility, with the following results: + +.. code:: shell + + bash-4.4$ time keycount.py + + Number of secret keys: 23 + Number of public keys: 12112 + + + real 11m52.945s + user 0m0.913s + sys 0m0.752s + + bash-4.4$ + +Sometime after that I imported another key and followed it with a little +test of Cython. This test was kept fairly basic, essentially lifting the +material from the `Cython Basic +Tutorial <http://docs.cython.org/en/latest/src/tutorial/cython_tutorial.html>`__ +to demonstrate compiling Python code to C. The first step was to take +the example key counting code quoted previously, essentially from the +importing of the ``gpg`` module to the end of the script: + +.. code:: python + + import gpg + + c = gpg.Context() + seckeys = c.keylist(pattern=None, secret=True) + pubkeys = c.keylist(pattern=None, secret=False) + + seclist = list(seckeys) + secnum = len(seclist) + + publist = list(pubkeys) + pubnum = len(publist) + + print(""" + Number of secret keys: {0} + Number of public keys: {1} + + """.format(secnum, pubnum)) + +Save that into a file called ``keycount.pyx`` and then create a +``setup.py`` file which contains this: + +.. code:: python + + from distutils.core import setup + from Cython.Build import cythonize + + setup( + ext_modules = cythonize("keycount.pyx") + ) + +Compile it: + +.. code:: shell + + bash-4.4$ python setup.py build_ext --inplace + bash-4.4$ + +Then run it in a similar manner to ``keycount.py``: + +.. code:: shell + + bash-4.4$ time python3.7 -c "import keycount" + + Number of secret keys: 23 + Number of public keys: 12113 + + + real 6m47.905s + user 0m0.785s + sys 0m0.331s + + bash-4.4$ + +Cython turned ``keycount.pyx`` into an 81KB ``keycount.o`` file in the +``build/`` directory, a 24KB ``keycount.cpython-37m-darwin.so`` file to +be imported into Python 3.7 and a 113KB ``keycount.c`` generated C +source code file of nearly three thousand lines. Quite a bit bigger than +the 314 bytes of the ``keycount.pyx`` file or the full 1,452 bytes of +the full executable ``keycount.py`` example script. + +On the other hand it ran in nearly half the time; taking 6 minutes and +47.905 seconds to run. As opposed to the 11 minutes and 52.945 seconds +which the CPython script alone took. + +The ``keycount.pyx`` and ``setup.py`` files used to generate this +example have been added to the ``examples/howto/advanced/cython/`` +directory The example versions include some additional options to +annotate the existing code and to detect Cython\'s use. The latter comes +from the `Magic +Attributes <http://docs.cython.org/en/latest/src/tutorial/pure.html#magic-attributes-within-the-pxd>`__ +section of the Cython documentation. + +.. _cheats-and-hacks: + +Miscellaneous extras and work-arounds +===================================== + +Most of the things in the following sections are here simply because +there was no better place to put them, even though some are only +peripherally related to the GPGME Python bindings. Some are also +workarounds for functions not integrated with GPGME as yet. This is +especially true of the first of these, dealing with `group +lines <#group-lines>`__. + +Group lines +----------- + +There is not yet an easy way to access groups configured in the gpg.conf +file from within GPGME. As a consequence these central groupings of keys +cannot be shared amongst multiple programs, such as MUAs readily. + +The following code, however, provides a work-around for obtaining this +information in Python. + +.. code:: python + + import subprocess + import sys + + if sys.platform == "win32": + gpgconfcmd = "gpgconf.exe --list-options gpg" + else: + gpgconfcmd = "gpgconf --list-options gpg" + + try: + lines = subprocess.getoutput(gpgconfcmd).splitlines() + except: + process = subprocess.Popen(gpgconfcmd.split(), stdout=subprocess.PIPE) + procom = process.communicate() + if sys.version_info[0] == 2: + lines = procom[0].splitlines() + else: + lines = procom[0].decode().splitlines() + + for i in range(len(lines)): + if lines[i].startswith("group") is True: + line = lines[i] + else: + pass + + groups = line.split(":")[-1].replace('"', '').split(',') + + group_lines = [] + group_lists = [] + + for i in range(len(groups)): + group_lines.append(groups[i].split("=")) + group_lists.append(groups[i].split("=")) + + for i in range(len(group_lists)): + group_lists[i][1] = group_lists[i][1].split() + +The result of that code is that ``group_lines`` is a list of lists where +``group_lines[i][0]`` is the name of the group and ``group_lines[i][1]`` +is the key IDs of the group as a string. + +The ``group_lists`` result is very similar in that it is a list of +lists. The first part, ``group_lists[i][0]`` matches +``group_lines[i][0]`` as the name of the group, but +``group_lists[i][1]`` is the key IDs of the group as a string. + +A demonstration of using the ``groups.py`` module is also available in +the form of the executable ``mutt-groups.py`` script. This second script +reads all the group entries in a user\'s ``gpg.conf`` file and converts +them into crypt-hooks suitable for use with the Mutt and Neomutt mail +clients. + +.. _hkp4py: + +Keyserver access for Python +--------------------------- + +The `hkp4py <https://github.com/Selfnet/hkp4py>`__ module by Marcel Fest +was originally a port of the old +`python-hkp <https://github.com/dgladkov/python-hkp>`__ module from +Python 2 to Python 3 and updated to use the +`requests <http://docs.python-requests.org/en/latest/index.html>`__ +module instead. It has since been modified to provide support for Python +2.7 as well and is available via PyPI. + +Since it rewrites the ``hkp`` protocol prefix as ``http`` and ``hkps`` +as ``https``, the module is able to be used even with servers which do +not support the full scope of keyserver functions. [5]_ It also works +quite readily when incorporated into a `Cython <#cython>`__ generated +and compiled version of any code. + +.. _hkp4py-strings: + +Key import format +~~~~~~~~~~~~~~~~~ + +The hkp4py module returns key data via requests as string literals +(``r.text``) instead of byte literals (``r.content``). This means that +the retrurned key data must be encoded to UTF-8 when importing that key +material using a ``gpg.Context().key_import()`` method. + +For this reason an alternative method has been added to the ``search`` +function of ``hkp4py.KeyServer()`` which returns the key in the correct +format as expected by ``key_import``. When importing using this module, +it is now possible to import with this: + +.. code:: python + + for key in keys: + if key.revoked is False: + gpg.Context().key_import(key.key_blob) + else: + pass + +Without that recent addition it would have been necessary to encode the +contents of each ``hkp4py.KeyServer().search()[i].key`` in +``hkp4py.KeyServer().search()`` before trying to import it. + +An example of this is included in the `Importing +Keys <#howto-import-key>`__ section of this HOWTO and the corresponding +executable version of that example is available in the +``lang/python/examples/howto`` directory as normal; the executable +version is the ``import-keys-hkp.py`` file. + +.. _copyright-and-license: + +Copyright and Licensing +======================= + +Copyright +--------- + +Copyright © The GnuPG Project, 2018. + +Copyright (C) The GnuPG Project, 2018. + +.. _draft-editions: + +Draft Editions of this HOWTO +---------------------------- + +Draft editions of this HOWTO may be periodically available directly from +the author at any of the following URLs: + +- `GPGME Python Bindings HOWTO draft (XHTML AWS S3 + SSL) <https://files.au.adversary.org/crypto/gpgme-python-howto.html>`__ +- `GPGME Python Bindings HOWTO draft (XHTML AWS S3 no + SSL) <http://files.au.adversary.org/crypto/gpgme-python-howto.html>`__ +- `GPGME Python Bindings HOWTO draft (Texinfo file AWS S3 + SSL) <https://files.au.adversary.org/crypto/gpgme-python-howto.texi>`__ +- `GPGME Python Bindings HOWTO draft (Texinfo file AWS S3 no + SSL) <http://files.au.adversary.org/crypto/gpgme-python-howto.texi>`__ +- `GPGME Python Bindings HOWTO draft (Info file AWS S3 + SSL) <https://files.au.adversary.org/crypto/gpgme-python-howto.info>`__ +- `GPGME Python Bindings HOWTO draft (Info file AWS S3 no + SSL) <http://files.au.adversary.org/crypto/gpgme-python-howto.info>`__ +- `GPGME Python Bindings HOWTO draft (reST file AWS S3 + SSL) <https://files.au.adversary.org/crypto/gpgme-python-howto.rst>`__ +- `GPGME Python Bindings HOWTO draft (reST file AWS S3 no + SSL) <http://files.au.adversary.org/crypto/gpgme-python-howto.rst>`__ +- `GPGME Python Bindings HOWTO draft (Docbook 4.2 AWS S3 + SSL) <https://files.au.adversary.org/crypto/gpgme-python-howto.xml>`__ +- `GPGME Python Bindings HOWTO draft (Docbook 4.2 AWS S3 no + SSL) <http://files.au.adversary.org/crypto/gpgme-python-howto.xml>`__ + +All of these draft versions except for one have been generated from this +document via Emacs `Org mode <https://orgmode.org/>`__ and `GNU +Texinfo <https://www.gnu.org/software/texinfo/>`__. Though it is likely +that the specific +`file <https://files.au.adversary.org/crypto/gpgme-python-howto.org>`__ +`version <http://files.au.adversary.org/crypto/gpgme-python-howto.org>`__ +used will be on the same server with the generated output formats. + +The one exception is the reStructuredText version, which was converted +using the latest version of Pandoc from the Org mode source file using +the following command: + +.. code:: shell + + pandoc -f org -t rst+smart -o gpgme-python-howto.rst gpgme-python-howto.org + +In addition to these there is a significantly less frequently updated +version as a HTML `WebHelp +site <https://files.au.adversary.org/crypto/gpgme-python-howto/webhelp/index.html>`__ +(AWS S3 SSL); generated from DITA XML source files, which can be found +in `an alternative +branch <https://dev.gnupg.org/source/gpgme/browse/ben%252Fhowto-dita/>`__ +of the GPGME git repository. + +These draft editions are not official documents and the version of +documentation in the master branch or which ships with released versions +is the only official documentation. Nevertheless, these draft editions +may occasionally be of use by providing more accessible web versions +which are updated between releases. They are provided on the +understanding that they may contain errors or may contain content +subject to change prior to an official release. + +.. _license: + +License GPL compatible +---------------------- + +This file is free software; as a special exception the author gives +unlimited permission to copy and/or distribute it, with or without +modifications, as long as this notice is preserved. + +This file is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY, to the extent permitted by law; without even the implied +warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +Footnotes +========= + +.. [1] + ``short-history.org`` and/or ``short-history.html``. + +.. [2] + With no issues reported specific to Python 3.7, the release of Python + 3.7.1 at around the same time as GPGME 1.12.0 and the testing with + Python 3.7.1rc1, there is no reason to delay moving 3.7 ahead of 3.6 + now. Production environments with more conservative requirements will + always enforce their own policies anyway and installation to each + supported minor release is quite possible too. + +.. [3] + Yes, even if you use virtualenv with everything you do in Python. If + you want to install this module as just your user account then you + will need to manually configure, compile and install the *entire* + GnuPG stack as that user as well. This includes libraries which are + not often installed that way. It can be done and there are + circumstances under which it is worthwhile, but generally only on + POSIX systems which utilise single user mode (some even require it). + +.. [4] + You probably don\'t really want to do this. Searching the keyservers + for \"gnupg.org\" produces over 400 results, the majority of which + aren\'t actually at the gnupg.org domain, but just included a comment + regarding the project in their key somewhere. + +.. [5] + Such as with ProtonMail servers. This also means that restricted + servers which only advertise either HTTP or HTTPS end points and not + HKP or HKPS end points must still be identified as as HKP or HKPS + within the Python Code. The ``hkp4py`` module will rewrite these + appropriately when the connection is made to the server. diff --git a/lang/python/doc/rst/index.rst b/lang/python/doc/rst/index.rst new file mode 100644 index 0000000..31dc146 --- /dev/null +++ b/lang/python/doc/rst/index.rst @@ -0,0 +1,12 @@ +.. _index: + +GPGME Python Bindings +===================== + +.. _index-contents: + +Contents +-------- + +- `A short history of the project <short-history.org>`__ +- `GPGME Python Bindings HOWTO <gpgme-python-howto.org>`__ diff --git a/lang/python/doc/rst/short-history.rst b/lang/python/doc/rst/short-history.rst new file mode 100644 index 0000000..8cf604f --- /dev/null +++ b/lang/python/doc/rst/short-history.rst @@ -0,0 +1,152 @@ +Overview +======== + +The GPGME Python bindings passed through many hands and numerous phases +before, after a fifteen year journey, coming full circle to return to +the source. This is a short explanation of that journey. + +.. _in-the-begining: + +In the beginning +---------------- + +In 2002 John Goerzen released PyME; Python bindings for the GPGME module +which utilised the current release of Python of the time and SWIG. [1]_ +Shortly after creating it and ensuring it worked he stopped supporting +it, though he left his work available on his Gopher site. + +Keeping the flame alive +----------------------- + +A couple of years later the project was picked up by Igor Belyi and +actively developed and maintained by him from 2004 to 2008. Igor's +whereabouts at the time of this document's creation are unknown, but the +current authors do hope he is well. We're assuming (or hoping) that life +did what life does and made continuing untenable. + +Passing the torch +----------------- + +In 2014 Martin Albrecht wanted to patch a bug in the PyME code and +discovered the absence of Igor. Following a discussion on the PyME +mailing list he became the new maintainer for PyME, releasing version +0.9.0 in May of that year. He remains the maintainer of the original +PyME release in Python 2.6 and 2.7 (available via PyPI). + +.. _ouroboros: + +Coming full circle +------------------ + +In 2015 Ben McGinnes approached Martin about a Python 3 version, while +investigating how complex a task this would be the task ended up being +completed. A subsequent discussion with Werner Koch led to the decision +to fold the Python 3 port back into the original GPGME release in the +languages subdirectory for non-C bindings under the module name of +``pyme3``. + +In 2016 this PyME module was integrated back into the GPGME project by +Justus Winter. During the course of this work Justus adjusted the port +to restore limited support for Python 2, but not as many minor point +releases as the original PyME package supports. During the course of +this integration the package was renamed to more accurately reflect its +status as a component of GPGME. The ``pyme3`` module was renamed to +``gpg`` and adopted by the upstream GnuPG team. + +In 2017 Justus departed G10code and the GnuPG team. Following this Ben +returned to maintain of gpgme Python bindings and continue building them +from that point. + +.. _relics-past: + +Relics of the past +================== + +There are a few things, in addition to code specific factors, such as +SWIG itself, which are worth noting here. + +The Annoyances of Git +--------------------- + +As anyone who has ever worked with git knows, submodules are horrible +way to deal with pretty much anything. In the interests of avoiding +migraines, that was skipped with addition of the PyME code to GPGME. + +Instead the files were added to a subdirectory of the ``lang/`` +directory, along with a copy of the entire git log up to that point as a +separate file within the ``lang/python/docs/`` directory. [2]_ As the +log for PyME is nearly 100KB and the log for GPGME is approximately 1MB, +this would cause considerable bloat, as well as some confusion, should +the two be merged. + +Hence the unfortunate, but necessary, step to simply move the files. A +regular repository version has been maintained should it be possible to +implement this better in the future. + +The Perils of PyPI +------------------ + +The early port of the Python 2 ``pyme`` module as ``pyme3`` was never +added to PyPI while the focus remained on development and testing during +2015 and early 2016. Later in 2016, however, when Justus completed his +major integration work and subsequently renamed the module from +``pyme3`` to ``gpg``, some prior releases were also provided through +PyPI. + +Since these bindings require a matching release of the GPGME libraries +in order to function, it was determined that there was little benefit in +also providing a copy through PyPI since anyone obtaining the GPGME +source code would obtain the Python bindings source code at the same +time. Whereas there was the potential to sew confusion amongst Python +users installing the module from PyPI, only to discover that without the +relevant C files, header files or SWIG compiled binaries, the Python +module did them little good. + +There are only two files on PyPI which might turn up in a search for +this module or a sample of its content: + +#. gpg (1.8.0) - Python bindings for GPGME GnuPG cryptography library +#. pyme (0.9.0) - Python support for GPGME GnuPG cryptography library + +.. _pypi-gpgme-180: + +GPG 1·8·0 - Python bindings for GPGME GnuPG cryptography library +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is the most recent version to reach PyPI and is the version of the +official Pyhon bindings which shipped with GPGME 1.8.0. If you have +GPGME 1.8.0 installed and *only* 1.8.0 installed, then it is probably +safe to use this copy from PyPI. + +As there have been a lot of changes since the release of GPGME 1.8.0, +the GnuPG Project recommends not using this version of the module and +instead installing the current version of GPGME along with the Python +bindings included with that package. + +.. _pypi-gpgme-90: + +PyME 0·9·0 - Python support for GPGME GnuPG cryptography library +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is the last release of the PyME bindings maintained by Martin +Albrecht and is only compatible with Python 2, it will not work with +Python 3. This is the version of the software from which the port from +Python 2 to Python 3 code was made in 2015. + +Users of the more recent Python bindings will recognise numerous points +of similarity, but also significant differences. It is likely that the +more recent official bindings will feel "more pythonic." + +For those using Python 2, there is essentially no harm in using this +module, but it may lack a number of more recent features added to GPGME. + +Footnotes +========= + +.. [1] + In all likelihood thos would have been Python 2.2 or possibly Python + 2.3. + +.. [2] + The entire PyME git log and other preceding VCS logs are located in + the ``gpgme/lang/python/docs/old-commits.log`` file. diff --git a/lang/python/doc/src/gpgme-python-howto.org b/lang/python/doc/src/gpgme-python-howto.org new file mode 100644 index 0000000..caa8e2f --- /dev/null +++ b/lang/python/doc/src/gpgme-python-howto.org @@ -0,0 +1,3043 @@ +#+TITLE: GNU Privacy Guard (GnuPG) Made Easy Python Bindings HOWTO (English) +#+AUTHOR: Ben McGinnes +#+LATEX_COMPILER: xelatex +#+LATEX_CLASS: article +#+LATEX_CLASS_OPTIONS: [12pt] +#+LATEX_HEADER: \usepackage{xltxtra} +#+LATEX_HEADER: \usepackage[margin=1in]{geometry} +#+LATEX_HEADER: \setmainfont[Ligatures={Common}]{Times New Roman} +#+LATEX_HEADER: \author{Ben McGinnes <ben@gnupg.org>} + + +* Introduction + :PROPERTIES: + :CUSTOM_ID: intro + :END: + +| Version: | 0.1.4 | +| GPGME Version: | 1.12.0 | +| Author: | [[https://gnupg.org/people/index.html#sec-1-5][Ben McGinnes]] <ben@gnupg.org> | +| Author GPG Key: | DB4724E6FA4286C92B4E55C4321E4E2373590E5D | +| Language: | Australian English, British English | +| xml:lang: | en-AU, en-GB, en | + +This document provides basic instruction in how to use the GPGME +Python bindings to programmatically leverage the GPGME library. + + +** Python 2 versus Python 3 + :PROPERTIES: + :CUSTOM_ID: py2-vs-py3 + :END: + +Though the GPGME Python bindings themselves provide support for both +Python 2 and 3, the focus is unequivocally on Python 3 and +specifically from Python 3.4 and above. As a consequence all the +examples and instructions in this guide use Python 3 code. + +Much of it will work with Python 2, but much of it also deals with +Python 3 byte literals, particularly when reading and writing data. +Developers concentrating on Python 2.7, and possibly even 2.6, will +need to make the appropriate modifications to support the older string +and unicode types as opposed to bytes. + +There are multiple reasons for concentrating on Python 3; some of +which relate to the immediate integration of these bindings, some of +which relate to longer term plans for both GPGME and the python +bindings and some of which relate to the impending EOL period for +Python 2.7. Essentially, though, there is little value in tying the +bindings to a version of the language which is a dead end and the +advantages offered by Python 3 over Python 2 make handling the data +types with which GPGME deals considerably easier. + + +** Examples + :PROPERTIES: + :CUSTOM_ID: howto-python3-examples + :END: + +All of the examples found in this document can be found as Python 3 +scripts in the =lang/python/examples/howto= directory. + + +** Unofficial Drafts + :PROPERTIES: + :CUSTOM_ID: unofficial-drafts + :END: + +In addition to shipping with each release of GPGME, there is a section +on locations to read or download [[#draft-editions][draft editions]] of this document from +at the end of it. These are unofficial versions produced in between +major releases. + + +** What's New + :PROPERTIES: + :CUSTOM_ID: new-stuff + :END: + +The most obviously new point for those reading this guide is this +section on other new things, but that's hardly important. Not given +all the other things which spurred the need for adding this section +and its subsections. + +*** New in GPGME 1·12·0 + :PROPERTIES: + :CUSTOM_ID: new-stuff-1-12-0 + :END: + +There have been quite a number of additions to GPGME and the Python +bindings to it since the last release of GPGME with versions 1.11.0 +and 1.11.1 in April, 2018. + +The bullet points of new additiions are: + +- an expanded section on [[#installation][installing]] and [[#snafu][troubleshooting]] the Python + bindings. +- The release of Python 3.7.0; which appears to be working just fine + with our bindings, in spite of intermittent reports of problems for + many other Python projects with that new release. +- Python 3.7 has been moved to the head of the specified python + versions list in the build process. +- In order to fix some other issues, there are certain underlying + functions which are more exposed through the [[#howto-get-context][gpg.Context()]], but + ongoing documentation ought to clarify that or otherwise provide the + best means of using the bindings. Some additions to =gpg.core= and + the =Context()=, however, were intended (see below). +- Continuing work in identifying and confirming the cause of + oft-reported [[#snafu-runtime-not-funtime][problems installing the Python bindings on Windows]]. +- GSOC: Google's Surreptitiously Ordered Conscription ... erm ... oh, + right; Google's Summer of Code. Though there were two hopeful + candidates this year; only one ended up involved with the GnuPG + Project directly, the other concentrated on an unrelated third party + project with closer ties to one of the GNU/Linux distributions than + to the GnuPG Project. Thus the Python bindings benefited from GSOC + participant Jacob Adams, who added the key_import function; building + on prior work by Tobias Mueller. +- Several new methods functions were added to the gpg.Context(), + including: [[#howto-import-key][key_import]], [[#howto-export-key][key_export]], [[#howto-export-public-key][key_export_minimal]] and + [[#howto-export-secret-key][key_export_secret]]. +- Importing and exporting examples include versions integrated with + Marcel Fest's recently released [[https://github.com/Selfnet/hkp4py][HKP for Python]] module. Some + [[#hkp4py][additional notes on this module]] are included at the end of the HOWTO. +- Instructions for dealing with semi-walled garden implementations + like ProtonMail are also included. This is intended to make things + a little easier when communicating with users of ProtonMail's + services and should not be construed as an endorsement of said + service. The GnuPG Project neither favours, nor disfavours + ProtonMail and the majority of this deals with interacting with the + ProtonMail keyserver. +- Semi-formalised the location where [[#draft-editions][draft versions]] of this HOWTO may + periodically be accessible. This is both for the reference of + others and testing the publishing of the document itself. Renamed + this file at around the same time. +- The Texinfo documentation build configuration has been replicated + from the parent project in order to make to maintain consistency + with that project (and actually ship with each release). +- a reStructuredText (=.rst=) version is also generated for Python + developers more used to and comfortable with that format as it is + the standard Python documentation format and Python developers may + wish to use it with Sphinx. Please note that there has been no + testing of the reStructuredText version with Sphinx at all. The + reST file was generated by the simple expedient of using [[https://pandoc.org/][Pandoc]]. +- Added a new section for [[#advanced-use][advanced or experimental use]]. +- Began the advanced use cases with [[#cython][a section]] on using the module with + [[http://cython.org/][Cython]]. +- Added a number of new scripts to the =example/howto/= directory; + some of which may be in advance of their planned sections of the + HOWTO (and some are just there because it seemed like a good idea at + the time). +- Cleaned up a lot of things under the hood. + + +* GPGME Concepts + :PROPERTIES: + :CUSTOM_ID: gpgme-concepts + :END: + + +** A C API + :PROPERTIES: + :CUSTOM_ID: gpgme-c-api + :END: + +Unlike many modern APIs with which programmers will be more familiar +with these days, the GPGME API is a C API. The API is intended for +use by C coders who would be able to access its features by including +the =gpgme.h= header file with their own C source code and then access +its functions just as they would any other C headers. + +This is a very effective method of gaining complete access to the API +and in the most efficient manner possible. It does, however, have the +drawback that it cannot be directly used by other languages without +some means of providing an interface to those languages. This is +where the need for bindings in various languages stems. + + +** Python bindings + :PROPERTIES: + :CUSTOM_ID: gpgme-python-bindings + :END: + +The Python bindings for GPGME provide a higher level means of +accessing the complete feature set of GPGME itself. It also provides +a more pythonic means of calling these API functions. + +The bindings are generated dynamically with SWIG and the copy of +=gpgme.h= generated when GPGME is compiled. + +This means that a version of the Python bindings is fundamentally tied +to the exact same version of GPGME used to generate that copy of +=gpgme.h=. + + +** Difference between the Python bindings and other GnuPG Python packages + :PROPERTIES: + :CUSTOM_ID: gpgme-python-bindings-diffs + :END: + +There have been numerous attempts to add GnuPG support to Python over +the years. Some of the most well known are listed here, along with +what differentiates them. + + +*** The python-gnupg package maintained by Vinay Sajip + :PROPERTIES: + :CUSTOM_ID: diffs-python-gnupg + :END: + +This is arguably the most popular means of integrating GPG with +Python. The package utilises the =subprocess= module to implement +wrappers for the =gpg= and =gpg2= executables normally invoked on the +command line (=gpg.exe= and =gpg2.exe= on Windows). + +The popularity of this package stemmed from its ease of use and +capability in providing the most commonly required features. + +Unfortunately it has been beset by a number of security issues in the +past; most of which stemmed from using unsafe methods of accessing the +command line via the =subprocess= calls. While some effort has been +made over the last two to three years (as of 2018) to mitigate this, +particularly by no longer providing shell access through those +subprocess calls, the wrapper is still somewhat limited in the scope +of its GnuPG features coverage. + +The python-gnupg package is available under the MIT license. + + +*** The gnupg package created and maintained by Isis Lovecruft + :PROPERTIES: + :CUSTOM_ID: diffs-isis-gnupg + :END: + +In 2015 Isis Lovecruft from the Tor Project forked and then +re-implemented the python-gnupg package as just gnupg. This new +package also relied on subprocess to call the =gpg= or =gpg2= +binaries, but did so somewhat more securely. + +The naming and version numbering selected for this package, however, +resulted in conflicts with the original python-gnupg and since its +functions were called in a different manner to python-gnupg, the +release of this package also resulted in a great deal of consternation +when people installed what they thought was an upgrade that +subsequently broke the code relying on it. + +The gnupg package is available under the GNU General Public License +version 3.0 (or any later version). + + +*** The PyME package maintained by Martin Albrecht + :PROPERTIES: + :CUSTOM_ID: diffs-pyme + :END: + +This package is the origin of these bindings, though they are somewhat +different now. For details of when and how the PyME package was +folded back into GPGME itself see the [[file:short-history.org][Short History]] document.[fn:1] + +The PyME package was first released in 2002 and was also the first +attempt to implement a low level binding to GPGME. In doing so it +provided access to considerably more functionality than either the +=python-gnupg= or =gnupg= packages. + +The PyME package is only available for Python 2.6 and 2.7. + +Porting the PyME package to Python 3.4 in 2015 is what resulted in it +being folded into the GPGME project and the current bindings are the +end result of that effort. + +The PyME package is available under the same dual licensing as GPGME +itself: the GNU General Public License version 2.0 (or any later +version) and the GNU Lesser General Public License version 2.1 (or any +later version). + + +* GPGME Python bindings installation + :PROPERTIES: + :CUSTOM_ID: gpgme-python-install + :END: + + +** No PyPI + :PROPERTIES: + :CUSTOM_ID: do-not-use-pypi + :END: + +Most third-party Python packages and modules are available and +distributed through the Python Package Installer, known as PyPI. + +Due to the nature of what these bindings are and how they work, it is +infeasible to install the GPGME Python bindings in the same way. + +This is because the bindings use SWIG to dynamically generate C +bindings against =gpgme.h= and =gpgme.h= is generated from +=gpgme.h.in= at compile time when GPGME is built from source. Thus to +include a package in PyPI which actually built correctly would require +either statically built libraries for every architecture bundled with +it or a full implementation of C for each architecture. + +See the additional notes regarding [[#snafu-cffi][CFFI and SWIG]] at the end of this +section for further details. + + +** Requirements + :PROPERTIES: + :CUSTOM_ID: gpgme-python-requirements + :END: + +The GPGME Python bindings only have three requirements: + +1. A suitable version of Python 2 or Python 3. With Python 2 that + means CPython 2.7 and with Python 3 that means CPython 3.4 or + higher. +2. [[https://www.swig.org][SWIG]]. +3. GPGME itself. Which also means that all of GPGME's dependencies + must be installed too. + + +*** Recommended Additions + :PROPERTIES: + :CUSTOM_ID: gpgme-python-recommendations + :END: + +Though none of the following are absolute requirements, they are all +recommended for use with the Python bindings. In some cases these +recommendations refer to which version(s) of CPython to use the +bindings with, while others refer to third party modules which provide +a significant advantage in some way. + +1. If possible, use Python 3 instead of 2. +2. Favour a more recent version of Python since even 3.4 is due to + reach EOL soon. In production systems and services, Python 3.6 + should be robust enough to be relied on. +3. If possible add the following Python modules which are not part of + the standard library: [[http://docs.python-requests.org/en/latest/index.html][Requests]], [[http://cython.org/][Cython]] and [[https://github.com/Selfnet/hkp4py][hkp4py]]. Chances are + quite high that at least the first one and maybe two of those will + already be installed. + +Note that, as with Cython, some of the planned additions to the +[[#advanced-use][Advanced]] section, will bring with them additional requirements. Most +of these will be fairly well known and commonly installed ones, +however, which are in many cases likely to have already been installed +on many systems or be familiar to Python programmers. + + +** Installation + :PROPERTIES: + :CUSTOM_ID: installation + :END: + +Installing the Python bindings is effectively achieved by compiling +and installing GPGME itself. + +Once SWIG is installed with Python and all the dependencies for GPGME +are installed you only need to confirm that the version(s) of Python +you want the bindings installed for are in your =$PATH=. + +By default GPGME will attempt to install the bindings for the most +recent or highest version number of Python 2 and Python 3 it detects +in =$PATH=. It specifically checks for the =python= and =python3= +executables first and then checks for specific version numbers. + +For Python 2 it checks for these executables in this order: =python=, +=python2= and =python2.7=. + +For Python 3 it checks for these executables in this order: =python3=, + =python3.7=, =python3.6=, =python3.5= and =python3.4=.[fn:2] + +On systems where =python= is actually =python3= and not =python2= it +may be possible that =python2= may be overlooked, but there have been +no reports of that actually occurring as yet. + +In the three months or so since the release of Python 3.7.0 there has +been extensive testing and work with these bindings with no issues +specifically relating to the new version of Python or any of the new +features of either the language or the bindings. This has also been +the case with Python 3.7.1rc1. With that in mind and given the +release of Python 3.7.1 is scheduled for around the same time as GPGME +1.12.0, the order of preferred Python versions has been changed to +move Python 3.7 ahead of Python 3.6. + + +*** Installing GPGME + :PROPERTIES: + :CUSTOM_ID: install-gpgme + :END: + +See the GPGME =README= file for details of how to install GPGME from +source. + + +** Known Issues + :PROPERTIES: + :CUSTOM_ID: snafu + :END: + +There are a few known issues with the current build process and the +Python bindings. For the most part these are easily addressed should +they be encountered. + + +*** Breaking Builds + :PROPERTIES: + :CUSTOM_ID: snafu-a-swig-of-this-builds-character + :END: + +Occasionally when installing GPGME with the Python bindings included +it may be observed that the =make= portion of that process induces a +large very number of warnings and, eventually errors which end that +part of the build process. Yet following that with =make check= and +=make install= appears to work seamlessly. + +The cause of this is related to the way SWIG needs to be called to +dynamically generate the C bindings for GPGME in the first place. So +the entire process will always produce =lang/python/python2-gpg/= and +=lang/python/python3-gpg/= directories. These should contain the +build output generated during compilation, including the complete +bindings and module installed into =site-packages=. + +Occasionally the errors in the early part or some other conflict +(e.g. not installing as */root/* or */su/*) may result in nothing +being installed to the relevant =site-packages= directory and the +build directory missing a lot of expected files. Even when this +occurs, the solution is actually quite simple and will always work. + +That solution is simply to run the following commands as either the +*root* user or prepended with =sudo -H=[fn:3] in the =lang/python/= +directory: + +#+BEGIN_SRC shell + /path/to/pythonX.Y setup.py build + /path/to/pythonX.Y setup.py build + /path/to/pythonX.Y setup.py install +#+END_SRC + +Yes, the build command does need to be run twice. Yes, you still need +to run the potentially failing or incomplete steps during the +=configure=, =make= and =make install= steps with installing GPGME. +This is because those steps generate a lot of essential files needed, +both by and in order to create, the bindings (including both the +=setup.py= and =gpgme.h= files). + + +**** IMPORTANT Note + :PROPERTIES: + :CUSTOM_ID: snafu-swig-build-note + :END: + +If specifying a selected number of languages to create bindings for, +try to leave Python last. Currently the majority of the other +language bindings are also preceding Python of either version when +listed alphabetically and so that just happens by default currently. + +If Python is set to precede one of the other languages then it is +possible that the errors described here may interrupt the build +process before generating bindings for those other languages. In +these cases it may be preferable to configure all preferred language +bindings separately with alternative =configure= steps for GPGME using +the =--enable-languages=$LANGUAGE= option. + + +*** Reinstalling Responsibly + :PROPERTIES: + :CUSTOM_ID: snafu-lessons-for-the-lazy + :END: + +Regardless of whether you're installing for one version of Python or +several, there will come a point where reinstallation is required. +With most Python module installations, the installed files go into the +relevant site-packages directory and are then forgotten about. Then +the module is upgraded, the new files are copied over the old and +that's the end of the matter. + +While the same is true of these bindings, there have been intermittent +issues observed on some platforms which have benefited significantly +from removing all the previous installations of the bindings before +installing the updated versions. + +Removing the previous version(s) is simply a matter of changing to the +relevant =site-packages= directory for the version of Python in +question and removing the =gpg/= directory and any accompanying +egg-info files for that module. + +In most cases this will require root or administration privileges on +the system, but the same is true of installing the module in the first +place. + + +*** Multiple installations + :PROPERTIES: + :CUSTOM_ID: snafu-the-full-monty + :END: + +For a veriety of reasons it may be either necessary or just preferable +to install the bindings to alternative installed Python versions which +meet the requirements of these bindings. + +On POSIX systems this will generally be most simply achieved by +running the manual installation commands (build, build, install) as +described in the previous section for each Python installation the +bindings need to be installed to. + +As per the SWIG documentation: the compilers, libraries and runtime +used to build GPGME and the Python Bindings *must* match those used to +compile Python itself, including the version number(s) (at least going +by major version numbers and probably minor numbers too). + +On most POSIX systems, including OS X, this will very likely be the +case in most, if not all, cases. + + +*** Won't Work With Windows + :PROPERTIES: + :CUSTOM_ID: snafu-runtime-not-funtime + :END: + +There are semi-regular reports of Windows users having considerable +difficulty in installing and using the Python bindings at all. Very +often, possibly even always, these reports come from Cygwin users +and/or MinGW users and/or Msys2 users. Though not all of them have +been confirmed, it appears that these reports have also come from +people who installed Python using the Windows installer files from the +[[https://python.org][Python website]] (i.e. mostly MSI installers, sometimes self-extracting +=.exe= files). + +The Windows versions of Python are not built using Cygwin, MinGW or +Msys2; they're built using Microsoft Visual Studio. Furthermore the +version used is /considerably/ more advanced than the version which +MinGW obtained a small number of files from many years ago in order to +be able to compile anything at all. Not only that, but there are +changes to the version of Visual Studio between some micro releases, +though that is is particularly the case with Python 2.7, since it has +been kept around far longer than it should have been. + +There are two theoretical solutions to this issue: + + 1. Compile and install the GnuPG stack, including GPGME and the + Python bibdings using the same version of Microsoft Visual Studio + used by the Python Foundation to compile the version of Python + installed. + + If there are multiple versions of Python then this will need to be + done with each different version of Visual Studio used. + + 2. Compile and install Python using the same tools used by choice, + such as MinGW or Msys2. + +Do *not* use the official Windows installer for Python unless +following the first method. + +In this type of situation it may even be for the best to accept that +there are less limitations on permissive software than free software +and simply opt to use a recent version of the Community Edition of +Microsoft Visual Studio to compile and build all of it, no matter +what. + +Investigations into the extent or the limitations of this issue are +ongoing. + + +*** CFFI is the Best™ and GPGME should use it instead of SWIG + :PROPERTIES: + :CUSTOM_ID: snafu-cffi + :END: + +There are many reasons for favouring [[https://cffi.readthedocs.io/en/latest/overview.html][CFFI]] and proponents of it are +quite happy to repeat these things as if all it would take to switch +from SWIG to CFFI is repeating that list as if it were a new concept. + +The fact is that there are things which Python's CFFI implementation +cannot handle in the GPGME C code. Beyond that there are features of +SWIG which are simply not available with CFFI at all. SWIG generates +the bindings to Python using the =gpgme.h= file, but that file is not +a single version shipped with each release, it too is generated when +GPGME is compiled. + +CFFI is currently unable to adapt to such a potentially mutable +codebase. If there were some means of applying SWIG's dynamic code +generation to produce the Python/CFFI API modes of accessing the GPGME +libraries (or the source source code directly), but such a thing does +not exist yet either and it currently appears that work is needed in +at least one of CFFI's dependencies before any of this can be +addressed. + +So if you're a massive fan of CFFI; that's great, but if you want this +project to switch to CFFI then rather than just insisting that it +should, I'd suggest you volunteer to bring CFFI up to the level this +project needs. + +If you're actually seriously considering doing so, then I'd suggest +taking the =gpgme-tool.c= file in the GPGME =src/= directory and +getting that to work with any of the CFFI API methods (not the ABI +methods, they'll work with pretty much anything). When you start +running into trouble with "ifdefs" then you'll know what sort of +things are lacking. That doesn't even take into account the amount of +work saved via SWIG's code generation techniques either. + + +*** Virtualised Environments + :PROPERTIES: + :CUSTOM_ID: snafu-venv + :END: + +It is fairly common practice amongst Python developers to, as much as +possible, use packages like virtualenv to keep various things that are +to be installed from interfering with each other. Given how much of +the GPGME bindings is often at odds with the usual pythonic way of +doing things, it stands to reason that this would be called into +question too. + +As it happens the answer as to whether or not the bindings can be used +with virtualenv, the answer is both yes and no. + +In general we recommend installing to the relevant path and matching +prefix of GPGME itself. Which means that when GPGME, and ideally the +rest of the GnuPG stack, is installed to a prefix like =/usr/local= or +=/opt/local= then the bindings would need to be installed to the main +Python installation and not a virtualised abstraction. Attempts to +separate the two in the past have been known to cause weird and +intermittent errors ranging from minor annoyances to complete failures +in the build process. + +As a consequence we only recommend building with and installing to the +main Python installations within the same prefix as GPGME is installed +to or which are found by GPGME's configuration stage immediately prior +to running the make commands. Which is exactly what the compiling and +installing process of GPGME does by default. + +Once that is done, however, it appears that a copy the compiled module +may be installed into a virtualenv of the same major and minor version +matching the build. Alternatively it is possible to utilise a +=sites.pth= file in the =site-packages/= directory of a viertualenv +installation, which links back to the system installations +corresponding directory in order to import anything installed system +wide. This may or may not be appropriate on a case by case basis. + +Though extensive testing of either of these options is not yet +complete, preliminary testing of them indicates that both are viable +as long as the main installation is complete. Which means that +certain other options normally restricted to virtual environments are +also available, including integration with pythonic test suites +(e.g. [[https://docs.pytest.org/en/latest/index.html][pytest]]) and other large projects. + +That said, it is worth reiterating the warning regarding non-standard +installations. If one were to attempt to install the bindings only to +a virtual environment without somehow also including the full GnuPG +stack (or enough of it as to include GPGME) then it is highly likely +that errors would be encountered at some point and more than a little +likely that the build process itself would break. + +If a degree of separation from the main operating system is still +required in spite of these warnings, then consider other forms of +virtualisation. Either a virtual machine (e.g. [[https://www.virtualbox.org/][VirtualBox]]), a +hardware emulation layer (e.g. [[https://www.qemu.org/][QEMU]]) or an application container +(e.g. [[https://www.docker.com/why-docker][Docker]]). + +Finally it should be noted that the limited tests conducted thus far +have been using the =virtualenv= command in a new directory to create +the virtual python environment. As opposed to the standard =python3 +-m venv= and it is possible that this will make a difference depending +on the system and version of Python in use. Another option is to run +the command =python3 -m virtualenv /path/to/install/virtual/thingy= +instead. + + +* Fundamentals + :PROPERTIES: + :CUSTOM_ID: howto-fund-a-mental + :END: + +Before we can get to the fun stuff, there are a few matters regarding +GPGME's design which hold true whether you're dealing with the C code +directly or these Python bindings. + + +** No REST + :PROPERTIES: + :CUSTOM_ID: no-rest-for-the-wicked + :END: + +The first part of which is or will be fairly blatantly obvious upon +viewing the first example, but it's worth reiterating anyway. That +being that this API is /*not*/ a REST API. Nor indeed could it ever +be one. + +Most, if not all, Python programmers (and not just Python programmers) +know how easy it is to work with a RESTful API. In fact they've +become so popular that many other APIs attempt to emulate REST-like +behaviour as much as they are able. Right down to the use of JSON +formatted output to facilitate the use of their API without having to +retrain developers. + +This API does not do that. It would not be able to do that and also +provide access to the entire C API on which it's built. It does, +however, provide a very pythonic interface on top of the direct +bindings and it's this pythonic layer that this HOWTO deals with. + + +** Context + :PROPERTIES: + :CUSTOM_ID: howto-get-context + :END: + +One of the reasons which prevents this API from being RESTful is that +most operations require more than one instruction to the API to +perform the task. Sure, there are certain functions which can be +performed simultaneously, particularly if the result known or strongly +anticipated (e.g. selecting and encrypting to a key known to be in the +public keybox). + +There are many more, however, which cannot be manipulated so readily: +they must be performed in a specific sequence and the result of one +operation has a direct bearing on the outcome of subsequent +operations. Not merely by generating an error either. + +When dealing with this type of persistent state on the web, full of +both the RESTful and REST-like, it's most commonly referred to as a +session. In GPGME, however, it is called a context and every +operation type has one. + + +* Working with keys + :PROPERTIES: + :CUSTOM_ID: howto-keys + :END: + + +** Key selection + :PROPERTIES: + :CUSTOM_ID: howto-keys-selection + :END: + +Selecting keys to encrypt to or to sign with will be a common +occurrence when working with GPGMe and the means available for doing +so are quite simple. + +They do depend on utilising a Context; however once the data is +recorded in another variable, that Context does not need to be the +same one which subsequent operations are performed. + +The easiest way to select a specific key is by searching for that +key's key ID or fingerprint, preferably the full fingerprint without +any spaces in it. A long key ID will probably be okay, but is not +advised and short key IDs are already a problem with some being +generated to match specific patterns. It does not matter whether the +pattern is upper or lower case. + +So this is the best method: + +#+BEGIN_SRC python -i +import gpg + +k = gpg.Context().keylist(pattern="258E88DCBD3CD44D8E7AB43F6ECB6AF0DEADBEEF") +keys = list(k) +#+END_SRC + +This is passable and very likely to be common: + +#+BEGIN_SRC python -i +import gpg + +k = gpg.Context().keylist(pattern="0x6ECB6AF0DEADBEEF") +keys = list(k) +#+END_SRC + +And this is a really bad idea: + +#+BEGIN_SRC python -i +import gpg + +k = gpg.Context().keylist(pattern="0xDEADBEEF") +keys = list(k) +#+END_SRC + +Alternatively it may be that the intention is to create a list of keys +which all match a particular search string. For instance all the +addresses at a particular domain, like this: + +#+BEGIN_SRC python -i +import gpg + +ncsc = gpg.Context().keylist(pattern="ncsc.mil") +nsa = list(ncsc) +#+END_SRC + + +*** Counting keys + :PROPERTIES: + :CUSTOM_ID: howto-keys-counting + :END: + +Counting the number of keys in your public keybox (=pubring.kbx=), the +format which has superseded the old keyring format (=pubring.gpg= and +=secring.gpg=), or the number of secret keys is a very simple task. + +#+BEGIN_SRC python -i +import gpg + +c = gpg.Context() +seckeys = c.keylist(pattern=None, secret=True) +pubkeys = c.keylist(pattern=None, secret=False) + +seclist = list(seckeys) +secnum = len(seclist) + +publist = list(pubkeys) +pubnum = len(publist) + +print(""" + Number of secret keys: {0} + Number of public keys: {1} +""".format(secnum, pubnum)) +#+END_SRC + +NOTE: The [[#cython][Cython]] introduction in the [[#advanced-use][Advanced and Experimental]] +section uses this same key counting code with Cython to demonstrate +some areas where Cython can improve performance even with the +bindings. Users with large public keyrings or keyboxes, for instance, +should consider these options if they are comfortable with using +Cython. + + +** Get key + :PROPERTIES: + :CUSTOM_ID: howto-get-key + :END: + +An alternative method of getting a single key via its fingerprint is +available directly within a Context with =Context().get_key=. This is +the preferred method of selecting a key in order to modify it, sign or +certify it and for obtaining relevant data about a single key as a +part of other functions; when verifying a signature made by that key, +for instance. + +By default this method will select public keys, but it can select +secret keys as well. + +This first example demonstrates selecting the current key of Werner +Koch, which is due to expire at the end of 2018: + +#+BEGIN_SRC python -i +import gpg + +fingerprint = "80615870F5BAD690333686D0F2AD85AC1E42B367" +key = gpg.Context().get_key(fingerprint) +#+END_SRC + +Whereas this example demonstrates selecting the author's current key +with the =secret= key word argument set to =True=: + +#+BEGIN_SRC python -i +import gpg + +fingerprint = "DB4724E6FA4286C92B4E55C4321E4E2373590E5D" +key = gpg.Context().get_key(fingerprint, secret=True) +#+END_SRC + +It is, of course, quite possible to select expired, disabled and +revoked keys with this function, but only to effectively display +information about those keys. + +It is also possible to use both unicode or string literals and byte +literals with the fingerprint when getting a key in this way. + + +** Importing keys + :PROPERTIES: + :CUSTOM_ID: howto-import-key + :END: + +Importing keys is possible with the =key_import()= method and takes +one argument which is a bytes literal object containing either the +binary or ASCII armoured key data for one or more keys. + +The following example retrieves one or more keys from the SKS +keyservers via the web using the requests module. Since requests +returns the content as a bytes literal object, we can then use that +directly to import the resulting data into our keybox. + +#+BEGIN_SRC python -i +import gpg +import os.path +import requests + +c = gpg.Context() +url = "https://sks-keyservers.net/pks/lookup" +pattern = input("Enter the pattern to search for key or user IDs: ") +payload = {"op": "get", "search": pattern} + +r = requests.get(url, verify=True, params=payload) +result = c.key_import(r.content) + +if result is not None and hasattr(result, "considered") is False: + print(result) +elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" + The total number of keys considered for import was: {0} + + Number of keys revoked: {1} + Number of new signatures: {2} + Number of new subkeys: {3} + Number of new user IDs: {4} + Number of new secret keys: {5} + Number of unchanged keys: {6} + + The key IDs for all considered keys were: +""".format(num_keys, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print("{0}\n".format(result.imports[i].fpr)) +else: + pass +#+END_SRC + +NOTE: When searching for a key ID of any length or a fingerprint +(without spaces), the SKS servers require the the leading =0x= +indicative of hexadecimal be included. Also note that the old short +key IDs (e.g. =0xDEADBEEF=) should no longer be used due to the +relative ease by which such key IDs can be reproduced, as demonstrated +by the Evil32 Project in 2014 (which was subsequently exploited in +2016). + + +*** Working with ProtonMail + :PROPERTIES: + :CUSTOM_ID: import-protonmail + :END: + +Here is a variation on the example above which checks the constrained +ProtonMail keyserver for ProtonMail public keys. + +#+BEGIN_SRC python -i +import gpg +import requests +import sys + +print(""" +This script searches the ProtonMail key server for the specified key and +imports it. +""") + +c = gpg.Context(armor=True) +url = "https://api.protonmail.ch/pks/lookup" +ksearch = [] + +if len(sys.argv) >= 2: + keyterm = sys.argv[1] +else: + keyterm = input("Enter the key ID, UID or search string: ") + +if keyterm.count("@") == 2 and keyterm.startswith("@") is True: + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) +elif keyterm.count("@") == 1 and keyterm.startswith("@") is True: + ksearch.append("{0}@protonmail.com".format(keyterm[1:])) + ksearch.append("{0}@protonmail.ch".format(keyterm[1:])) + ksearch.append("{0}@pm.me".format(keyterm[1:])) +elif keyterm.count("@") == 0: + ksearch.append("{0}@protonmail.com".format(keyterm)) + ksearch.append("{0}@protonmail.ch".format(keyterm)) + ksearch.append("{0}@pm.me".format(keyterm)) +elif keyterm.count("@") == 2 and keyterm.startswith("@") is False: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) +elif keyterm.count("@") > 2: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) +else: + ksearch.append(keyterm) + +for k in ksearch: + payload = {"op": "get", "search": k} + try: + r = requests.get(url, verify=True, params=payload) + if r.ok is True: + result = c.key_import(r.content) + elif r.ok is False: + result = r.content + except Exception as e: + result = None + + if result is not None and hasattr(result, "considered") is False: + print("{0} for {1}".format(result.decode(), k)) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" +The total number of keys considered for import was: {0} + +With UIDs wholely or partially matching the following string: + + {1} + + Number of keys revoked: {2} + Number of new signatures: {3} + Number of new subkeys: {4} + Number of new user IDs: {5} +Number of new secret keys: {6} + Number of unchanged keys: {7} + +The key IDs for all considered keys were: +""".format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + elif result is None: + print(e) +#+END_SRC + +Both the above example, [[../examples/howto/pmkey-import.py][pmkey-import.py]], and a version which prompts +for an alternative GnuPG home directory, [[../examples/howto/pmkey-import-alt.py][pmkey-import-alt.py]], are +available with the other examples and are executable scripts. + +Note that while the ProtonMail servers are based on the SKS servers, +their server is related more to their API and is not feature complete +by comparison to the servers in the SKS pool. One notable difference +being that the ProtonMail server does not permit non ProtonMail users +to update their own keys, which could be a vector for attacking +ProtonMail users who may not receive a key's revocation if it had been +compromised. + + +*** Importing with HKP for Python + :PROPERTIES: + :CUSTOM_ID: import-hkp4py + :END: + +Performing the same tasks with the [[https://github.com/Selfnet/hkp4py][hkp4py module]] (available via PyPI) +is not too much different, but does provide a number of options of +benefit to end users. Not least of which being the ability to perform +some checks on a key before importing it or not. For instance it may +be the policy of a site or project to only import keys which have not +been revoked. The hkp4py module permits such checks prior to the +importing of the keys found. + +#+BEGIN_SRC python -i +import gpg +import hkp4py +import sys + +c = gpg.Context() +server = hkp4py.KeyServer("hkps://hkps.pool.sks-keyservers.net") +results = [] + +if len(sys.argv) > 2: + pattern = " ".join(sys.argv[1:]) +elif len(sys.argv) == 2: + pattern = sys.argv[1] +else: + pattern = input("Enter the pattern to search for keys or user IDs: ") + +try: + keys = server.search(pattern) + print("Found {0} key(s).".format(len(keys))) +except Exception as e: + keys = [] + for logrus in pattern.split(): + if logrus.startswith("0x") is True: + key = server.search(logrus) + else: + key = server.search("0x{0}".format(logrus)) + keys.append(key[0]) + print("Found {0} key(s).".format(len(keys))) + +for key in keys: + import_result = c.key_import(key.key_blob) + results.append(import_result) + +for result in results: + if result is not None and hasattr(result, "considered") is False: + print(result) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" +The total number of keys considered for import was: {0} + + Number of keys revoked: {1} + Number of new signatures: {2} + Number of new subkeys: {3} + Number of new user IDs: {4} +Number of new secret keys: {5} + Number of unchanged keys: {6} + +The key IDs for all considered keys were: +""".format(num_keys, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + else: + pass +#+END_SRC + +Since the hkp4py module handles multiple keys just as effectively as +one (=keys= is a list of responses per matching key), the example +above is able to do a little bit more with the returned data before +anything is actually imported. + + +*** Importing from ProtonMail with HKP for Python + :PROPERTIES: + :CUSTOM_ID: import-protonmail-hkp4py + :END: + +Though this can provide certain benefits even when working with +ProtonMail, the scope is somewhat constrained there due to the +limitations of the ProtonMail keyserver. + +For instance, searching the SKS keyserver pool for the term "gnupg" +produces hundreds of results from any time the word appears in any +part of a user ID. Performing the same search on the ProtonMail +keyserver returns zero results, even though there are at least two +test accounts which include it as part of the username. + +The cause of this discrepancy is the deliberate configuration of that +server by ProtonMail to require an exact match of the full email +address of the ProtonMail user whose key is being requested. +Presumably this is intended to reduce breaches of privacy of their +users as an email address must already be known before a key for that +address can be obtained. + + +**** Import from ProtonMail via HKP for Python Example no. 1 + :PROPERTIES: + :CUSTOM_ID: import-hkp4py-pm1 + :END: + +The following script is avalable with the rest of the examples under +the somewhat less than original name, =pmkey-import-hkp.py=. + +#+BEGIN_SRC python -i +import gpg +import hkp4py +import os.path +import sys + +print(""" +This script searches the ProtonMail key server for the specified key and +imports it. + +Usage: pmkey-import-hkp.py [search strings] +""") + +c = gpg.Context(armor=True) +server = hkp4py.KeyServer("hkps://api.protonmail.ch") +keyterms = [] +ksearch = [] +allkeys = [] +results = [] +paradox = [] +homeless = None + +if len(sys.argv) > 2: + keyterms = sys.argv[1:] +elif len(sys.argv) == 2: + keyterm = sys.argv[1] + keyterms.append(keyterm) +else: + key_term = input("Enter the key ID, UID or search string: ") + keyterms = key_term.split() + +for keyterm in keyterms: + if keyterm.count("@") == 2 and keyterm.startswith("@") is True: + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + elif keyterm.count("@") == 1 and keyterm.startswith("@") is True: + ksearch.append("{0}@protonmail.com".format(keyterm[1:])) + ksearch.append("{0}@protonmail.ch".format(keyterm[1:])) + ksearch.append("{0}@pm.me".format(keyterm[1:])) + elif keyterm.count("@") == 0: + ksearch.append("{0}@protonmail.com".format(keyterm)) + ksearch.append("{0}@protonmail.ch".format(keyterm)) + ksearch.append("{0}@pm.me".format(keyterm)) + elif keyterm.count("@") == 2 and keyterm.startswith("@") is False: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) + elif keyterm.count("@") > 2: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) + else: + ksearch.append(keyterm) + +for k in ksearch: + print("Checking for key for: {0}".format(k)) + try: + keys = server.search(k) + if isinstance(keys, list) is True: + for key in keys: + allkeys.append(key) + try: + import_result = c.key_import(key.key_blob) + except Exception as e: + import_result = c.key_import(key.key) + else: + paradox.append(keys) + import_result = None + except Exception as e: + import_result = None + results.append(import_result) + +for result in results: + if result is not None and hasattr(result, "considered") is False: + print("{0} for {1}".format(result.decode(), k)) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" +The total number of keys considered for import was: {0} + +With UIDs wholely or partially matching the following string: + + {1} + + Number of keys revoked: {2} + Number of new signatures: {3} + Number of new subkeys: {4} + Number of new user IDs: {5} +Number of new secret keys: {6} + Number of unchanged keys: {7} + +The key IDs for all considered keys were: +""".format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + elif result is None: + pass +#+END_SRC + + +**** Import from ProtonMail via HKP for Python Example no. 2 + :PROPERTIES: + :CUSTOM_ID: import-hkp4py-pm2 + :END: + +Like its counterpart above, this script can also be found with the +rest of the examples, by the name pmkey-import-hkp-alt.py. + +With this script a modicum of effort has been made to treat anything +passed as a =homedir= which either does not exist or which is not a +directory, as also being a pssible user ID to check for. It's not +guaranteed to pick up on all such cases, but it should cover most of +them. + +#+BEGIN_SRC python -i +import gpg +import hkp4py +import os.path +import sys + +print(""" +This script searches the ProtonMail key server for the specified key and +imports it. Optionally enables specifying a different GnuPG home directory. + +Usage: pmkey-import-hkp.py [homedir] [search string] + or: pmkey-import-hkp.py [search string] +""") + +c = gpg.Context(armor=True) +server = hkp4py.KeyServer("hkps://api.protonmail.ch") +keyterms = [] +ksearch = [] +allkeys = [] +results = [] +paradox = [] +homeless = None + +if len(sys.argv) > 3: + homedir = sys.argv[1] + keyterms = sys.argv[2:] +elif len(sys.argv) == 3: + homedir = sys.argv[1] + keyterm = sys.argv[2] + keyterms.append(keyterm) +elif len(sys.argv) == 2: + homedir = "" + keyterm = sys.argv[1] + keyterms.append(keyterm) +else: + keyterm = input("Enter the key ID, UID or search string: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + keyterms.append(keyterm) + +if len(homedir) == 0: + homedir = None + homeless = False + +if homedir is not None: + if homedir.startswith("~"): + if os.path.exists(os.path.expanduser(homedir)) is True: + if os.path.isdir(os.path.expanduser(homedir)) is True: + c.home_dir = os.path.realpath(os.path.expanduser(homedir)) + else: + homeless = True + else: + homeless = True + elif os.path.exists(os.path.realpath(homedir)) is True: + if os.path.isdir(os.path.realpath(homedir)) is True: + c.home_dir = os.path.realpath(homedir) + else: + homeless = True + else: + homeless = True + +# First check to see if the homedir really is a homedir and if not, treat it as +# a search string. +if homeless is True: + keyterms.append(homedir) + c.home_dir = None +else: + pass + +for keyterm in keyterms: + if keyterm.count("@") == 2 and keyterm.startswith("@") is True: + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + elif keyterm.count("@") == 1 and keyterm.startswith("@") is True: + ksearch.append("{0}@protonmail.com".format(keyterm[1:])) + ksearch.append("{0}@protonmail.ch".format(keyterm[1:])) + ksearch.append("{0}@pm.me".format(keyterm[1:])) + elif keyterm.count("@") == 0: + ksearch.append("{0}@protonmail.com".format(keyterm)) + ksearch.append("{0}@protonmail.ch".format(keyterm)) + ksearch.append("{0}@pm.me".format(keyterm)) + elif keyterm.count("@") == 2 and keyterm.startswith("@") is False: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) + elif keyterm.count("@") > 2: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) + else: + ksearch.append(keyterm) + +for k in ksearch: + print("Checking for key for: {0}".format(k)) + try: + keys = server.search(k) + if isinstance(keys, list) is True: + for key in keys: + allkeys.append(key) + try: + import_result = c.key_import(key.key_blob) + except Exception as e: + import_result = c.key_import(key.key) + else: + paradox.append(keys) + import_result = None + except Exception as e: + import_result = None + results.append(import_result) + +for result in results: + if result is not None and hasattr(result, "considered") is False: + print("{0} for {1}".format(result.decode(), k)) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" +The total number of keys considered for import was: {0} + +With UIDs wholely or partially matching the following string: + + {1} + + Number of keys revoked: {2} + Number of new signatures: {3} + Number of new subkeys: {4} + Number of new user IDs: {5} +Number of new secret keys: {6} + Number of unchanged keys: {7} + +The key IDs for all considered keys were: +""".format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + elif result is None: + pass +#+END_SRC + + +** Exporting keys + :PROPERTIES: + :CUSTOM_ID: howto-export-key + :END: + +Exporting keys remains a reasonably simple task, but has been +separated into three different functions for the OpenPGP cryptographic +engine. Two of those functions are for exporting public keys and the +third is for exporting secret keys. + + +*** Exporting public keys + :PROPERTIES: + :CUSTOM_ID: howto-export-public-key + :END: + +There are two methods of exporting public keys, both of which are very +similar to the other. The default method, =key_export()=, will export +a public key or keys matching a specified pattern as normal. The +alternative, the =key_export_minimal()= method, will do the same thing +except producing a minimised output with extra signatures and third +party signatures or certifications removed. + +#+BEGIN_SRC python -i +import gpg +import os.path +import sys + +print(""" +This script exports one or more public keys. +""") + +c = gpg.Context(armor=True) + +if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] +elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") +elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + keyfile = input("Enter the path and filename to save the secret key to: ") + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if homedir.startswith("~"): + if os.path.exists(os.path.expanduser(homedir)) is True: + c.home_dir = os.path.expanduser(homedir) + else: + pass +elif os.path.exists(homedir) is True: + c.home_dir = homedir +else: + pass + +try: + result = c.key_export(pattern=logrus) +except: + result = c.key_export(pattern=None) + +if result is not None: + with open(keyfile, "wb") as f: + f.write(result) +else: + pass +#+END_SRC + +It should be noted that the result will only return =None= when a +search pattern has been entered, but has not matched any keys. When +the search pattern itself is set to =None= this triggers the exporting +of the entire public keybox. + +#+BEGIN_SRC python -i +import gpg +import os.path +import sys + +print(""" +This script exports one or more public keys in minimised form. +""") + +c = gpg.Context(armor=True) + +if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] +elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") +elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + keyfile = input("Enter the path and filename to save the secret key to: ") + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if homedir.startswith("~"): + if os.path.exists(os.path.expanduser(homedir)) is True: + c.home_dir = os.path.expanduser(homedir) + else: + pass +elif os.path.exists(homedir) is True: + c.home_dir = homedir +else: + pass + +try: + result = c.key_export_minimal(pattern=logrus) +except: + result = c.key_export_minimal(pattern=None) + +if result is not None: + with open(keyfile, "wb") as f: + f.write(result) +else: + pass +#+END_SRC + + +*** Exporting secret keys + :PROPERTIES: + :CUSTOM_ID: howto-export-secret-key + :END: + +Exporting secret keys is, functionally, very similar to exporting +public keys; save for the invocation of =pinentry= via =gpg-agent= in +order to securely enter the key's passphrase and authorise the export. + +The following example exports the secret key to a file which is then +set with the same permissions as the output files created by the +command line secret key export options. + +#+BEGIN_SRC python -i +import gpg +import os +import os.path +import sys + +print(""" +This script exports one or more secret keys. + +The gpg-agent and pinentry are invoked to authorise the export. +""") + +c = gpg.Context(armor=True) + +if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] +elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") +elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + keyfile = input("Enter the path and filename to save the secret key to: ") + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if len(homedir) == 0: + homedir = None +elif homedir.startswith("~"): + userdir = os.path.expanduser(homedir) + if os.path.exists(userdir) is True: + homedir = os.path.realpath(userdir) + else: + homedir = None +else: + homedir = os.path.realpath(homedir) + +if os.path.exists(homedir) is False: + homedir = None +else: + if os.path.isdir(homedir) is False: + homedir = None + else: + pass + +if homedir is not None: + c.home_dir = homedir +else: + pass + +try: + result = c.key_export_secret(pattern=logrus) +except: + result = c.key_export_secret(pattern=None) + +if result is not None: + with open(keyfile, "wb") as f: + f.write(result) + os.chmod(keyfile, 0o600) +else: + pass +#+END_SRC + +Alternatively the approach of the following script can be used. This +longer example saves the exported secret key(s) in files in the GnuPG +home directory, in addition to setting the file permissions as only +readable and writable by the user. It also exports the secret key(s) +twice in order to output both GPG binary (=.gpg=) and ASCII armoured +(=.asc=) files. + +#+BEGIN_SRC python -i +import gpg +import os +import os.path +import subprocess +import sys + +print(""" +This script exports one or more secret keys as both ASCII armored and binary +file formats, saved in files within the user's GPG home directory. + +The gpg-agent and pinentry are invoked to authorise the export. +""") + +if sys.platform == "win32": + gpgconfcmd = "gpgconf.exe --list-dirs homedir" +else: + gpgconfcmd = "gpgconf --list-dirs homedir" + +a = gpg.Context(armor=True) +b = gpg.Context() +c = gpg.Context() + +if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] +elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") +elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + keyfile = input("Enter the filename to save the secret key to: ") + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if len(homedir) == 0: + homedir = None +elif homedir.startswith("~"): + userdir = os.path.expanduser(homedir) + if os.path.exists(userdir) is True: + homedir = os.path.realpath(userdir) + else: + homedir = None +else: + homedir = os.path.realpath(homedir) + +if os.path.exists(homedir) is False: + homedir = None +else: + if os.path.isdir(homedir) is False: + homedir = None + else: + pass + +if homedir is not None: + c.home_dir = homedir +else: + pass + +if c.home_dir is not None: + if c.home_dir.endswith("/"): + gpgfile = "{0}{1}.gpg".format(c.home_dir, keyfile) + ascfile = "{0}{1}.asc".format(c.home_dir, keyfile) + else: + gpgfile = "{0}/{1}.gpg".format(c.home_dir, keyfile) + ascfile = "{0}/{1}.asc".format(c.home_dir, keyfile) +else: + if os.path.exists(os.environ["GNUPGHOME"]) is True: + hd = os.environ["GNUPGHOME"] + else: + try: + hd = subprocess.getoutput(gpgconfcmd) + except: + process = subprocess.Popen(gpgconfcmd.split(), + stdout=subprocess.PIPE) + procom = process.communicate() + if sys.version_info[0] == 2: + hd = procom[0].strip() + else: + hd = procom[0].decode().strip() + gpgfile = "{0}/{1}.gpg".format(hd, keyfile) + ascfile = "{0}/{1}.asc".format(hd, keyfile) + +try: + a_result = a.key_export_secret(pattern=logrus) + b_result = b.key_export_secret(pattern=logrus) +except: + a_result = a.key_export_secret(pattern=None) + b_result = b.key_export_secret(pattern=None) + +if a_result is not None: + with open(ascfile, "wb") as f: + f.write(a_result) + os.chmod(ascfile, 0o600) +else: + pass + +if b_result is not None: + with open(gpgfile, "wb") as f: + f.write(b_result) + os.chmod(gpgfile, 0o600) +else: + pass +#+END_SRC + + +*** Sending public keys to the SKS Keyservers + :PROPERTIES: + :CUSTOM_ID: howto-send-public-key + :END: + +As with the previous section on importing keys, the =hkp4py= module +adds another option with exporting keys in order to send them to the +public keyservers. + +The following example demonstrates how this may be done. + +#+BEGIN_SRC python -i +import gpg +import hkp4py +import os.path +import sys + +print(""" +This script sends one or more public keys to the SKS keyservers and is +essentially a slight variation on the export-key.py script. +""") + +c = gpg.Context(armor=True) +server = hkp4py.KeyServer("hkps://hkps.pool.sks-keyservers.net") + +if len(sys.argv) > 2: + logrus = " ".join(sys.argv[1:]) +elif len(sys.argv) == 2: + logrus = sys.argv[1] +else: + logrus = input("Enter the UID matching the key(s) to send: ") + +if len(logrus) > 0: + try: + export_result = c.key_export(pattern=logrus) + except Exception as e: + print(e) + export_result = None +else: + export_result = c.key_export(pattern=None) + +if export_result is not None: + try: + try: + send_result = server.add(export_result) + except: + send_result = server.add(export_result.decode()) + if send_result is not None: + print(send_result) + else: + pass + except Exception as e: + print(e) +else: + pass +#+END_SRC + +An expanded version of this script with additional functions for +specifying an alternative homedir location is in the examples +directory as =send-key-to-keyserver.py=. + +The =hkp4py= module appears to handle both string and byte literal text +data equally well, but the GPGME bindings deal primarily with byte +literal data only and so this script sends in that format first, then +tries the string literal form. + + +* Basic Functions + :PROPERTIES: + :CUSTOM_ID: howto-the-basics + :END: + +The most frequently called features of any cryptographic library will +be the most fundamental tasks for encryption software. In this +section we will look at how to programmatically encrypt data, decrypt +it, sign it and verify signatures. + + +** Encryption + :PROPERTIES: + :CUSTOM_ID: howto-basic-encryption + :END: + +Encrypting is very straight forward. In the first example below the +message, =text=, is encrypted to a single recipient's key. In the +second example the message will be encrypted to multiple recipients. + + +*** Encrypting to one key + :PROPERTIES: + :CUSTOM_ID: howto-basic-encryption-single + :END: + +Once the the Context is set the main issues with encrypting data is +essentially reduced to key selection and the keyword arguments +specified in the =gpg.Context().encrypt()= method. + +Those keyword arguments are: =recipients=, a list of keys encrypted to +(covered in greater detail in the following section); =sign=, whether +or not to sign the plaintext data, see subsequent sections on signing +and verifying signatures below (defaults to =True=); =sink=, to write +results or partial results to a secure sink instead of returning it +(defaults to =None=); =passphrase=, only used when utilising symmetric +encryption (defaults to =None=); =always_trust=, used to override the +trust model settings for recipient keys (defaults to =False=); +=add_encrypt_to=, utilises any preconfigured =encrypt-to= or +=default-key= settings in the user's =gpg.conf= file (defaults to +=False=); =prepare=, prepare for encryption (defaults to =False=); +=expect_sign=, prepare for signing (defaults to =False=); =compress=, +compresses the plaintext prior to encryption (defaults to =True=). + +#+BEGIN_SRC python -i +import gpg + +a_key = "0x12345678DEADBEEF" +text = b"""Some text to test with. + +Since the text in this case must be bytes, it is most likely that +the input form will be a separate file which is opened with "rb" +as this is the simplest method of obtaining the correct data format. +""" + +c = gpg.Context(armor=True) +rkey = list(c.keylist(pattern=a_key, secret=False)) +ciphertext, result, sign_result = c.encrypt(text, recipients=rkey, sign=False) + +with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) +#+END_SRC + +Though this is even more likely to be used like this; with the +plaintext input read from a file, the recipient keys used for +encryption regardless of key trust status and the encrypted output +also encrypted to any preconfigured keys set in the =gpg.conf= file: + +#+BEGIN_SRC python -i +import gpg + +a_key = "0x12345678DEADBEEF" + +with open("secret_plans.txt", "rb") as afile: + text = afile.read() + +c = gpg.Context(armor=True) +rkey = list(c.keylist(pattern=a_key, secret=False)) +ciphertext, result, sign_result = c.encrypt(text, recipients=rkey, sign=True, + always_trust=True, + add_encrypt_to=True) + +with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) +#+END_SRC + +If the =recipients= paramater is empty then the plaintext is encrypted +symmetrically. If no =passphrase= is supplied as a parameter or via a +callback registered with the =Context()= then an out-of-band prompt +for the passphrase via pinentry will be invoked. + + +*** Encrypting to multiple keys + :PROPERTIES: + :CUSTOM_ID: howto-basic-encryption-multiple + :END: + +Encrypting to multiple keys essentially just expands upon the key +selection process and the recipients from the previous examples. + +The following example encrypts a message (=text=) to everyone with an +email address on the =gnupg.org= domain,[fn:4] but does /not/ encrypt +to a default key or other key which is configured to normally encrypt +to. + +#+BEGIN_SRC python -i +import gpg + +text = b"""Oh look, another test message. + +The same rules apply as with the previous example and more likely +than not, the message will actually be drawn from reading the +contents of a file or, maybe, from entering data at an input() +prompt. + +Since the text in this case must be bytes, it is most likely that +the input form will be a separate file which is opened with "rb" +as this is the simplest method of obtaining the correct data +format. +""" + +c = gpg.Context(armor=True) +rpattern = list(c.keylist(pattern="@gnupg.org", secret=False)) +logrus = [] + +for i in range(len(rpattern)): + if rpattern[i].can_encrypt == 1: + logrus.append(rpattern[i]) + +ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, + sign=False, always_trust=True) + +with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) +#+END_SRC + +All it would take to change the above example to sign the message +and also encrypt the message to any configured default keys would +be to change the =c.encrypt= line to this: + +#+BEGIN_SRC python -i +ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, + always_trust=True, + add_encrypt_to=True) +#+END_SRC + +The only keyword arguments requiring modification are those for which +the default values are changing. The default value of =sign= is +=True=, the default of =always_trust= is =False=, the default of +=add_encrypt_to= is =False=. + +If =always_trust= is not set to =True= and any of the recipient keys +are not trusted (e.g. not signed or locally signed) then the +encryption will raise an error. It is possible to mitigate this +somewhat with something more like this: + +#+BEGIN_SRC python -i +import gpg + +with open("secret_plans.txt.asc", "rb") as afile: + text = afile.read() + +c = gpg.Context(armor=True) +rpattern = list(c.keylist(pattern="@gnupg.org", secret=False)) +logrus = [] + +for i in range(len(rpattern)): + if rpattern[i].can_encrypt == 1: + logrus.append(rpattern[i]) + + try: + ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, + add_encrypt_to=True) + except gpg.errors.InvalidRecipients as e: + for i in range(len(e.recipients)): + for n in range(len(logrus)): + if logrus[n].fpr == e.recipients[i].fpr: + logrus.remove(logrus[n]) + else: + pass + try: + ciphertext, result, sign_result = c.encrypt(text, + recipients=logrus, + add_encrypt_to=True) + with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) + except: + pass +#+END_SRC + +This will attempt to encrypt to all the keys searched for, then remove +invalid recipients if it fails and try again. + + +** Decryption + :PROPERTIES: + :CUSTOM_ID: howto-basic-decryption + :END: + +Decrypting something encrypted to a key in one's secret keyring is +fairly straight forward. + +In this example code, however, preconfiguring either =gpg.Context()= +or =gpg.core.Context()= as =c= is unnecessary because there is no need +to modify the Context prior to conducting the decryption and since the +Context is only used once, setting it to =c= simply adds lines for no +gain. + +#+BEGIN_SRC python -i +import gpg + +ciphertext = input("Enter path and filename of encrypted file: ") +newfile = input("Enter path and filename of file to save decrypted data to: ") + +with open(ciphertext, "rb") as cfile: + try: + plaintext, result, verify_result = gpg.Context().decrypt(cfile) + except gpg.errors.GPGMEError as e: + plaintext = None + print(e) + +if plaintext is not None: + with open(newfile, "wb") as nfile: + nfile.write(plaintext) + else: + pass +#+END_SRC + +The data available in =plaintext= in this example is the decrypted +content as a byte object, the recipient key IDs and algorithms in +=result= and the results of verifying any signatures of the data in +=verify_result=. + + +** Signing text and files + :PROPERTIES: + :CUSTOM_ID: howto-basic-signing + :END: + +The following sections demonstrate how to specify keys to sign with. + + +*** Signing key selection + :PROPERTIES: + :CUSTOM_ID: howto-basic-signing-signers + :END: + +By default GPGME and the Python bindings will use the default key +configured for the user invoking the GPGME API. If there is no +default key specified and there is more than one secret key available +it may be necessary to specify the key or keys with which to sign +messages and files. + +#+BEGIN_SRC python -i +import gpg + +logrus = input("Enter the email address or string to match signing keys to: ") +hancock = gpg.Context().keylist(pattern=logrus, secret=True) +sig_src = list(hancock) +#+END_SRC + +The signing examples in the following sections include the explicitly +designated =signers= parameter in two of the five examples; once where +the resulting signature would be ASCII armoured and once where it +would not be armoured. + +While it would be possible to enter a key ID or fingerprint here to +match a specific key, it is not possible to enter two fingerprints and +match two keys since the patten expects a string, bytes or None and +not a list. A string with two fingerprints won't match any single +key. + + +*** Normal or default signing messages or files + :PROPERTIES: + :CUSTOM_ID: howto-basic-signing-normal + :END: + +The normal or default signing process is essentially the same as is +most often invoked when also encrypting a message or file. So when +the encryption component is not utilised, the result is to produce an +encoded and signed output which may or may not be ASCII armoured and +which may or may not also be compressed. + +By default compression will be used unless GnuPG detects that the +plaintext is already compressed. ASCII armouring will be determined +according to the value of =gpg.Context().armor=. + +The compression algorithm is selected in much the same way as the +symmetric encryption algorithm or the hash digest algorithm is when +multiple keys are involved; from the preferences saved into the key +itself or by comparison with the preferences with all other keys +involved. + +#+BEGIN_SRC python -i +import gpg + +text0 = """Declaration of ... something. + +""" +text = text0.encode() + +c = gpg.Context(armor=True, signers=sig_src) +signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.NORMAL) + +with open("/path/to/statement.txt.asc", "w") as afile: + afile.write(signed_data.decode()) +#+END_SRC + +Though everything in this example is accurate, it is more likely that +reading the input data from another file and writing the result to a +new file will be performed more like the way it is done in the next +example. Even if the output format is ASCII armoured. + +#+BEGIN_SRC python -i +import gpg + +with open("/path/to/statement.txt", "rb") as tfile: + text = tfile.read() + +c = gpg.Context() +signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.NORMAL) + +with open("/path/to/statement.txt.sig", "wb") as afile: + afile.write(signed_data) +#+END_SRC + + +*** Detached signing messages and files + :PROPERTIES: + :CUSTOM_ID: howto-basic-signing-detached + :END: + +Detached signatures will often be needed in programmatic uses of +GPGME, either for signing files (e.g. tarballs of code releases) or as +a component of message signing (e.g. PGP/MIME encoded email). + +#+BEGIN_SRC python -i +import gpg + +text0 = """Declaration of ... something. + +""" +text = text0.encode() + +c = gpg.Context(armor=True) +signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.DETACH) + +with open("/path/to/statement.txt.asc", "w") as afile: + afile.write(signed_data.decode()) +#+END_SRC + +As with normal signatures, detached signatures are best handled as +byte literals, even when the output is ASCII armoured. + +#+BEGIN_SRC python -i +import gpg + +with open("/path/to/statement.txt", "rb") as tfile: + text = tfile.read() + +c = gpg.Context(signers=sig_src) +signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.DETACH) + +with open("/path/to/statement.txt.sig", "wb") as afile: + afile.write(signed_data) +#+END_SRC + + +*** Clearsigning messages or text + :PROPERTIES: + :CUSTOM_ID: howto-basic-signing-clear + :END: + +Though PGP/in-line messages are no longer encouraged in favour of +PGP/MIME, there is still sometimes value in utilising in-line +signatures. This is where clear-signed messages or text is of value. + +#+BEGIN_SRC python -i +import gpg + +text0 = """Declaration of ... something. + +""" +text = text0.encode() + +c = gpg.Context() +signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.CLEAR) + +with open("/path/to/statement.txt.asc", "w") as afile: + afile.write(signed_data.decode()) +#+END_SRC + +In spite of the appearance of a clear-signed message, the data handled +by GPGME in signing it must still be byte literals. + +#+BEGIN_SRC python -i +import gpg + +with open("/path/to/statement.txt", "rb") as tfile: + text = tfile.read() + +c = gpg.Context() +signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.CLEAR) + +with open("/path/to/statement.txt.asc", "wb") as afile: + afile.write(signed_data) +#+END_SRC + + +** Signature verification + :PROPERTIES: + :CUSTOM_ID: howto-basic-verification + :END: + +Essentially there are two principal methods of verification of a +signature. The first of these is for use with the normal or default +signing method and for clear-signed messages. The second is for use +with files and data with detached signatures. + +The following example is intended for use with the default signing +method where the file was not ASCII armoured: + +#+BEGIN_SRC python -i +import gpg +import time + +filename = "statement.txt" +gpg_file = "statement.txt.gpg" + +c = gpg.Context() + +try: + data, result = c.verify(open(gpg_file)) + verified = True +except gpg.errors.BadSignatures as e: + verified = False + print(e) + +if verified is True: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: +{0} +with key {1} +made at {2} +""".format(c.get_key(sign.fpr).uids[0].uid, sign.fpr, + time.ctime(sign.timestamp))) +else: + pass +#+END_SRC + +Whereas this next example, which is almost identical would work with +normal ASCII armoured files and with clear-signed files: + +#+BEGIN_SRC python -i +import gpg +import time + +filename = "statement.txt" +asc_file = "statement.txt.asc" + +c = gpg.Context() + +try: + data, result = c.verify(open(asc_file)) + verified = True +except gpg.errors.BadSignatures as e: + verified = False + print(e) + +if verified is True: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: +{0} +with key {1} +made at {2} +""".format(c.get_key(sign.fpr).uids[0].uid, sign.fpr, + time.ctime(sign.timestamp))) +else: + pass +#+END_SRC + +In both of the previous examples it is also possible to compare the +original data that was signed against the signed data in =data= to see +if it matches with something like this: + +#+BEGIN_SRC python -i +with open(filename, "rb") as afile: + text = afile.read() + +if text == data: + print("Good signature.") +else: + pass +#+END_SRC + +The following two examples, however, deal with detached signatures. +With his method of verification the data that was signed does not get +returned since it is already being explicitly referenced in the first +argument of =c.verify=. So =data= is =None= and only the information +in =result= is available. + +#+BEGIN_SRC python -i +import gpg +import time + +filename = "statement.txt" +sig_file = "statement.txt.sig" + +c = gpg.Context() + +try: + data, result = c.verify(open(filename), open(sig_file)) + verified = True +except gpg.errors.BadSignatures as e: + verified = False + print(e) + +if verified is True: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: +{0} +with key {1} +made at {2} +""".format(c.get_key(sign.fpr).uids[0].uid, sign.fpr, + time.ctime(sign.timestamp))) +else: + pass +#+END_SRC + +#+BEGIN_SRC python -i +import gpg +import time + +filename = "statement.txt" +asc_file = "statement.txt.asc" + +c = gpg.Context() + +try: + data, result = c.verify(open(filename), open(asc_file)) + verified = True +except gpg.errors.BadSignatures as e: + verified = False + print(e) + +if verified is True: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: +{0} +with key {1} +made at {2} +""".format(c.get_key(sign.fpr).uids[0].uid, sign.fpr, + time.ctime(sign.timestamp))) +else: + pass +#+END_SRC + + +* Creating keys and subkeys + :PROPERTIES: + :CUSTOM_ID: key-generation + :END: + +The one thing, aside from GnuPG itself, that GPGME depends on, of +course, is the keys themselves. So it is necessary to be able to +generate them and modify them by adding subkeys, revoking or disabling +them, sometimes deleting them and doing the same for user IDs. + +In the following examples a key will be created for the world's +greatest secret agent, Danger Mouse. Since Danger Mouse is a secret +agent he needs to be able to protect information to =SECRET= level +clearance, so his keys will be 3072-bit keys. + +The pre-configured =gpg.conf= file which sets cipher, digest and other +preferences contains the following configuration parameters: + +#+BEGIN_SRC conf + expert + allow-freeform-uid + allow-secret-key-import + trust-model tofu+pgp + tofu-default-policy unknown + enable-large-rsa + enable-dsa2 + cert-digest-algo SHA512 + default-preference-list TWOFISH CAMELLIA256 AES256 CAMELLIA192 AES192 CAMELLIA128 AES BLOWFISH IDEA CAST5 3DES SHA512 SHA384 SHA256 SHA224 RIPEMD160 SHA1 ZLIB BZIP2 ZIP Uncompressed + personal-cipher-preferences TWOFISH CAMELLIA256 AES256 CAMELLIA192 AES192 CAMELLIA128 AES BLOWFISH IDEA CAST5 3DES + personal-digest-preferences SHA512 SHA384 SHA256 SHA224 RIPEMD160 SHA1 + personal-compress-preferences ZLIB BZIP2 ZIP Uncompressed +#+END_SRC + + +** Primary key + :PROPERTIES: + :CUSTOM_ID: keygen-primary + :END: + +Generating a primary key uses the =create_key= method in a Context. +It contains multiple arguments and keyword arguments, including: +=userid=, =algorithm=, =expires_in=, =expires=, =sign=, =encrypt=, +=certify=, =authenticate=, =passphrase= and =force=. The defaults for +all of those except =userid=, =algorithm=, =expires_in=, =expires= and +=passphrase= is =False=. The defaults for =algorithm= and +=passphrase= is =None=. The default for =expires_in= is =0=. The +default for =expires= is =True=. There is no default for =userid=. + +If =passphrase= is left as =None= then the key will not be generated +with a passphrase, if =passphrase= is set to a string then that will +be the passphrase and if =passphrase= is set to =True= then gpg-agent +will launch pinentry to prompt for a passphrase. For the sake of +convenience, these examples will keep =passphrase= set to =None=. + +#+BEGIN_SRC python -i +import gpg + +c = gpg.Context() + +c.home_dir = "~/.gnupg-dm" +userid = "Danger Mouse <dm@secret.example.net>" + +dmkey = c.create_key(userid, algorithm="rsa3072", expires_in=31536000, + sign=True, certify=True) +#+END_SRC + +One thing to note here is the use of setting the =c.home_dir= +parameter. This enables generating the key or keys in a different +location. In this case to keep the new key data created for this +example in a separate location rather than adding it to existing and +active key store data. As with the default directory, =~/.gnupg=, any +temporary or separate directory needs the permissions set to only +permit access by the directory owner. On posix systems this means +setting the directory permissions to 700. + +The =temp-homedir-config.py= script in the HOWTO examples directory +will create an alternative homedir with these configuration options +already set and the correct directory and file permissions. + +The successful generation of the key can be confirmed via the returned +=GenkeyResult= object, which includes the following data: + +#+BEGIN_SRC python -i +print(""" + Fingerprint: {0} + Primary Key: {1} + Public Key: {2} + Secret Key: {3} + Sub Key: {4} +User IDs: {5} +""".format(dmkey.fpr, dmkey.primary, dmkey.pubkey, dmkey.seckey, dmkey.sub, + dmkey.uid)) +#+END_SRC + +Alternatively the information can be confirmed using the command line +program: + +#+BEGIN_SRC shell + bash-4.4$ gpg --homedir ~/.gnupg-dm -K + ~/.gnupg-dm/pubring.kbx + ---------------------- + sec rsa3072 2018-03-15 [SC] [expires: 2019-03-15] + 177B7C25DB99745EE2EE13ED026D2F19E99E63AA + uid [ultimate] Danger Mouse <dm@secret.example.net> + + bash-4.4$ +#+END_SRC + +As with generating keys manually, to preconfigure expanded preferences +for the cipher, digest and compression algorithms, the =gpg.conf= file +must contain those details in the home directory in which the new key +is being generated. I used a cut down version of my own =gpg.conf= +file in order to be able to generate this: + +#+BEGIN_SRC shell + bash-4.4$ gpg --homedir ~/.gnupg-dm --edit-key 177B7C25DB99745EE2EE13ED026D2F19E99E63AA showpref quit + Secret key is available. + + sec rsa3072/026D2F19E99E63AA + created: 2018-03-15 expires: 2019-03-15 usage: SC + trust: ultimate validity: ultimate + [ultimate] (1). Danger Mouse <dm@secret.example.net> + + [ultimate] (1). Danger Mouse <dm@secret.example.net> + Cipher: TWOFISH, CAMELLIA256, AES256, CAMELLIA192, AES192, CAMELLIA128, AES, BLOWFISH, IDEA, CAST5, 3DES + Digest: SHA512, SHA384, SHA256, SHA224, RIPEMD160, SHA1 + Compression: ZLIB, BZIP2, ZIP, Uncompressed + Features: MDC, Keyserver no-modify + + bash-4.4$ +#+END_SRC + + +** Subkeys + :PROPERTIES: + :CUSTOM_ID: keygen-subkeys + :END: + +Adding subkeys to a primary key is fairly similar to creating the +primary key with the =create_subkey= method. Most of the arguments +are the same, but not quite all. Instead of the =userid= argument +there is now a =key= argument for selecting which primary key to add +the subkey to. + +In the following example an encryption subkey will be added to the +primary key. Since Danger Mouse is a security conscious secret agent, +this subkey will only be valid for about six months, half the length +of the primary key. + +#+BEGIN_SRC python -i +import gpg + +c = gpg.Context() +c.home_dir = "~/.gnupg-dm" + +key = c.get_key(dmkey.fpr, secret=True) +dmsub = c.create_subkey(key, algorithm="rsa3072", expires_in=15768000, + encrypt=True) +#+END_SRC + +As with the primary key, the results here can be checked with: + +#+BEGIN_SRC python -i +print(""" + Fingerprint: {0} + Primary Key: {1} + Public Key: {2} + Secret Key: {3} + Sub Key: {4} +User IDs: {5} +""".format(dmsub.fpr, dmsub.primary, dmsub.pubkey, dmsub.seckey, dmsub.sub, + dmsub.uid)) +#+END_SRC + +As well as on the command line with: + +#+BEGIN_SRC shell + bash-4.4$ gpg --homedir ~/.gnupg-dm -K + ~/.gnupg-dm/pubring.kbx + ---------------------- + sec rsa3072 2018-03-15 [SC] [expires: 2019-03-15] + 177B7C25DB99745EE2EE13ED026D2F19E99E63AA + uid [ultimate] Danger Mouse <dm@secret.example.net> + ssb rsa3072 2018-03-15 [E] [expires: 2018-09-13] + + bash-4.4$ +#+END_SRC + + +** User IDs + :PROPERTIES: + :CUSTOM_ID: keygen-uids + :END: + + +*** Adding User IDs + :PROPERTIES: + :CUSTOM_ID: keygen-uids-add + :END: + +By comparison to creating primary keys and subkeys, adding a new user +ID to an existing key is much simpler. The method used to do this is +=key_add_uid= and the only arguments it takes are for the =key= and +the new =uid=. + +#+BEGIN_SRC python -i +import gpg + +c = gpg.Context() +c.home_dir = "~/.gnupg-dm" + +dmfpr = "177B7C25DB99745EE2EE13ED026D2F19E99E63AA" +key = c.get_key(dmfpr, secret=True) +uid = "Danger Mouse <danger.mouse@secret.example.net>" + +c.key_add_uid(key, uid) +#+END_SRC + +Unsurprisingly the result of this is: + +#+BEGIN_SRC shell + bash-4.4$ gpg --homedir ~/.gnupg-dm -K + ~/.gnupg-dm/pubring.kbx + ---------------------- + sec rsa3072 2018-03-15 [SC] [expires: 2019-03-15] + 177B7C25DB99745EE2EE13ED026D2F19E99E63AA + uid [ultimate] Danger Mouse <danger.mouse@secret.example.net> + uid [ultimate] Danger Mouse <dm@secret.example.net> + ssb rsa3072 2018-03-15 [E] [expires: 2018-09-13] + + bash-4.4$ +#+END_SRC + + +*** Revokinging User IDs + :PROPERTIES: + :CUSTOM_ID: keygen-uids-revoke + :END: + +Revoking a user ID is a fairly similar process, except that it uses +the =key_revoke_uid= method. + +#+BEGIN_SRC python -i +import gpg + +c = gpg.Context() +c.home_dir = "~/.gnupg-dm" + +dmfpr = "177B7C25DB99745EE2EE13ED026D2F19E99E63AA" +key = c.get_key(dmfpr, secret=True) +uid = "Danger Mouse <danger.mouse@secret.example.net>" + +c.key_revoke_uid(key, uid) +#+END_SRC + + +** Key certification + :PROPERTIES: + :CUSTOM_ID: key-sign + :END: + +Since key certification is more frequently referred to as key signing, +the method used to perform this function is =key_sign=. + +The =key_sign= method takes four arguments: =key=, =uids=, +=expires_in= and =local=. The default value of =uids= is =None= and +which results in all user IDs being selected. The default value of +both =expires_in= and =local= is =False=; which results in the +signature never expiring and being able to be exported. + +The =key= is the key being signed rather than the key doing the +signing. To change the key doing the signing refer to the signing key +selection above for signing messages and files. + +If the =uids= value is not =None= then it must either be a string to +match a single user ID or a list of strings to match multiple user +IDs. In this case the matching of those strings must be precise and +it is case sensitive. + +To sign Danger Mouse's key for just the initial user ID with a +signature which will last a little over a month, do this: + +#+BEGIN_SRC python -i +import gpg + +c = gpg.Context() +uid = "Danger Mouse <dm@secret.example.net>" + +dmfpr = "177B7C25DB99745EE2EE13ED026D2F19E99E63AA" +key = c.get_key(dmfpr, secret=True) +c.key_sign(key, uids=uid, expires_in=2764800) +#+END_SRC + + +* Advanced or Experimental Use Cases + :PROPERTIES: + :CUSTOM_ID: advanced-use + :END: + + +** C plus Python plus SWIG plus Cython + :PROPERTIES: + :CUSTOM_ID: cython + :END: + +In spite of the apparent incongruence of using Python bindings to a C +interface only to generate more C from the Python; it is in fact quite +possible to use the GPGME bindings with [[http://docs.cython.org/en/latest/index.html][Cython]]. Though in many cases +the benefits may not be obvious since the most computationally +intensive work never leaves the level of the C code with which GPGME +itself is interacting with. + +Nevertheless, there are some situations where the benefits are +demonstrable. One of the better and easier examples being the one of +the early examples in this HOWTO, the [[#howto-keys-counting][key counting]] code. Running that +example as an executable Python script, =keycount.py= (available in +the =examples/howto/= directory), will take a noticable amount of time +to run on most systems where the public keybox or keyring contains a +few thousand public keys. + +Earlier in the evening, prior to starting this section, I ran that +script on my laptop; as I tend to do periodically and timed it using +=time= utility, with the following results: + +#+BEGIN_SRC shell + bash-4.4$ time keycount.py + + Number of secret keys: 23 + Number of public keys: 12112 + + + real 11m52.945s + user 0m0.913s + sys 0m0.752s + + bash-4.4$ +#+END_SRC + +Sometime after that I imported another key and followed it with a +little test of Cython. This test was kept fairly basic, essentially +lifting the material from the [[http://docs.cython.org/en/latest/src/tutorial/cython_tutorial.html][Cython Basic Tutorial]] to demonstrate +compiling Python code to C. The first step was to take the example +key counting code quoted previously, essentially from the importing of +the =gpg= module to the end of the script: + +#+BEGIN_SRC python -i +import gpg + +c = gpg.Context() +seckeys = c.keylist(pattern=None, secret=True) +pubkeys = c.keylist(pattern=None, secret=False) + +seclist = list(seckeys) +secnum = len(seclist) + +publist = list(pubkeys) +pubnum = len(publist) + +print(""" + Number of secret keys: {0} + Number of public keys: {1} + +""".format(secnum, pubnum)) +#+END_SRC + +Save that into a file called =keycount.pyx= and then create a +=setup.py= file which contains this: + +#+BEGIN_SRC python -i +from distutils.core import setup +from Cython.Build import cythonize + +setup( + ext_modules = cythonize("keycount.pyx") +) +#+END_SRC + +Compile it: + +#+BEGIN_SRC shell + bash-4.4$ python setup.py build_ext --inplace + bash-4.4$ +#+END_SRC + +Then run it in a similar manner to =keycount.py=: + +#+BEGIN_SRC shell + bash-4.4$ time python3.7 -c "import keycount" + + Number of secret keys: 23 + Number of public keys: 12113 + + + real 6m47.905s + user 0m0.785s + sys 0m0.331s + + bash-4.4$ +#+END_SRC + +Cython turned =keycount.pyx= into an 81KB =keycount.o= file in the +=build/= directory, a 24KB =keycount.cpython-37m-darwin.so= file to be +imported into Python 3.7 and a 113KB =keycount.c= generated C source +code file of nearly three thousand lines. Quite a bit bigger than the +314 bytes of the =keycount.pyx= file or the full 1,452 bytes of the +full executable =keycount.py= example script. + +On the other hand it ran in nearly half the time; taking 6 minutes and +47.905 seconds to run. As opposed to the 11 minutes and 52.945 seconds +which the CPython script alone took. + +The =keycount.pyx= and =setup.py= files used to generate this example +have been added to the =examples/howto/advanced/cython/= directory +The example versions include some additional options to annotate the +existing code and to detect Cython's use. The latter comes from the +[[http://docs.cython.org/en/latest/src/tutorial/pure.html#magic-attributes-within-the-pxd][Magic Attributes]] section of the Cython documentation. + + +* Miscellaneous extras and work-arounds + :PROPERTIES: + :CUSTOM_ID: cheats-and-hacks + :END: + +Most of the things in the following sections are here simply because +there was no better place to put them, even though some are only +peripherally related to the GPGME Python bindings. Some are also +workarounds for functions not integrated with GPGME as yet. This is +especially true of the first of these, dealing with [[#group-lines][group lines]]. + + +** Group lines + :PROPERTIES: + :CUSTOM_ID: group-lines + :END: + +There is not yet an easy way to access groups configured in the +gpg.conf file from within GPGME. As a consequence these central +groupings of keys cannot be shared amongst multiple programs, such as +MUAs readily. + +The following code, however, provides a work-around for obtaining this +information in Python. + +#+BEGIN_SRC python -i +import subprocess +import sys + +if sys.platform == "win32": + gpgconfcmd = "gpgconf.exe --list-options gpg" +else: + gpgconfcmd = "gpgconf --list-options gpg" + +try: + lines = subprocess.getoutput(gpgconfcmd).splitlines() +except: + process = subprocess.Popen(gpgconfcmd.split(), stdout=subprocess.PIPE) + procom = process.communicate() + if sys.version_info[0] == 2: + lines = procom[0].splitlines() + else: + lines = procom[0].decode().splitlines() + +for i in range(len(lines)): + if lines[i].startswith("group") is True: + line = lines[i] + else: + pass + +groups = line.split(":")[-1].replace('"', '').split(',') + +group_lines = [] +group_lists = [] + +for i in range(len(groups)): + group_lines.append(groups[i].split("=")) + group_lists.append(groups[i].split("=")) + +for i in range(len(group_lists)): + group_lists[i][1] = group_lists[i][1].split() +#+END_SRC + +The result of that code is that =group_lines= is a list of lists where +=group_lines[i][0]= is the name of the group and =group_lines[i][1]= +is the key IDs of the group as a string. + +The =group_lists= result is very similar in that it is a list of +lists. The first part, =group_lists[i][0]= matches +=group_lines[i][0]= as the name of the group, but =group_lists[i][1]= +is the key IDs of the group as a string. + +A demonstration of using the =groups.py= module is also available in +the form of the executable =mutt-groups.py= script. This second +script reads all the group entries in a user's =gpg.conf= file and +converts them into crypt-hooks suitable for use with the Mutt and +Neomutt mail clients. + + +** Keyserver access for Python + :PROPERTIES: + :CUSTOM_ID: hkp4py + :END: + +The [[https://github.com/Selfnet/hkp4py][hkp4py]] module by Marcel Fest was originally a port of the old +[[https://github.com/dgladkov/python-hkp][python-hkp]] module from Python 2 to Python 3 and updated to use the +[[http://docs.python-requests.org/en/latest/index.html][requests]] module instead. It has since been modified to provide +support for Python 2.7 as well and is available via PyPI. + +Since it rewrites the =hkp= protocol prefix as =http= and =hkps= as +=https=, the module is able to be used even with servers which do not +support the full scope of keyserver functions.[fn:5] It also works quite +readily when incorporated into a [[#cython][Cython]] generated and compiled version +of any code. + + +*** Key import format + :PROPERTIES: + :CUSTOM_ID: hkp4py-strings + :END: + +The hkp4py module returns key data via requests as string literals +(=r.text=) instead of byte literals (=r.content=). This means that +the retrurned key data must be encoded to UTF-8 when importing that +key material using a =gpg.Context().key_import()= method. + +For this reason an alternative method has been added to the =search= +function of =hkp4py.KeyServer()= which returns the key in the correct +format as expected by =key_import=. When importing using this module, +it is now possible to import with this: + +#+BEGIN_SRC python -i +for key in keys: + if key.revoked is False: + gpg.Context().key_import(key.key_blob) + else: + pass +#+END_SRC + +Without that recent addition it would have been necessary to encode +the contents of each =hkp4py.KeyServer().search()[i].key= in +=hkp4py.KeyServer().search()= before trying to import it. + +An example of this is included in the [[#howto-import-key][Importing Keys]] section of this +HOWTO and the corresponding executable version of that example is +available in the =lang/python/examples/howto= directory as normal; the +executable version is the =import-keys-hkp.py= file. + + +* Copyright and Licensing + :PROPERTIES: + :CUSTOM_ID: copyright-and-license + :END: + + +** Copyright + :PROPERTIES: + :CUSTOM_ID: copyright + :END: + +Copyright © The GnuPG Project, 2018. + +Copyright (C) The GnuPG Project, 2018. + + +** Draft Editions of this HOWTO + :PROPERTIES: + :CUSTOM_ID: draft-editions + :END: + +Draft editions of this HOWTO may be periodically available directly +from the author at any of the following URLs: + +- [[https://files.au.adversary.org/crypto/gpgme-python-howto.html][GPGME Python Bindings HOWTO draft (XHTML AWS S3 SSL)]] +- [[http://files.au.adversary.org/crypto/gpgme-python-howto.html][GPGME Python Bindings HOWTO draft (XHTML AWS S3 no SSL)]] +- [[https://files.au.adversary.org/crypto/gpgme-python-howto.texi][GPGME Python Bindings HOWTO draft (Texinfo file AWS S3 SSL)]] +- [[http://files.au.adversary.org/crypto/gpgme-python-howto.texi][GPGME Python Bindings HOWTO draft (Texinfo file AWS S3 no SSL)]] +- [[https://files.au.adversary.org/crypto/gpgme-python-howto.info][GPGME Python Bindings HOWTO draft (Info file AWS S3 SSL)]] +- [[http://files.au.adversary.org/crypto/gpgme-python-howto.info][GPGME Python Bindings HOWTO draft (Info file AWS S3 no SSL)]] +- [[https://files.au.adversary.org/crypto/gpgme-python-howto.rst][GPGME Python Bindings HOWTO draft (reST file AWS S3 SSL)]] +- [[http://files.au.adversary.org/crypto/gpgme-python-howto.rst][GPGME Python Bindings HOWTO draft (reST file AWS S3 no SSL)]] +- [[https://files.au.adversary.org/crypto/gpgme-python-howto.xml][GPGME Python Bindings HOWTO draft (Docbook 4.2 AWS S3 SSL)]] +- [[http://files.au.adversary.org/crypto/gpgme-python-howto.xml][GPGME Python Bindings HOWTO draft (Docbook 4.2 AWS S3 no SSL)]] + +All of these draft versions except for one have been generated from +this document via Emacs [[https://orgmode.org/][Org mode]] and [[https://www.gnu.org/software/texinfo/][GNU Texinfo]]. Though it is likely +that the specific [[https://files.au.adversary.org/crypto/gpgme-python-howto.org][file]] [[http://files.au.adversary.org/crypto/gpgme-python-howto.org][version]] used will be on the same server with +the generated output formats. + +The one exception is the reStructuredText version, which was converted +using the latest version of Pandoc from the Org mode source file using +the following command: + +#+BEGIN_SRC shell + pandoc -f org -t rst+smart -o gpgme-python-howto.rst gpgme-python-howto.org +#+END_SRC + +In addition to these there is a significantly less frequently updated +version as a HTML [[https://files.au.adversary.org/crypto/gpgme-python-howto/webhelp/index.html][WebHelp site]] (AWS S3 SSL); generated from DITA XML +source files, which can be found in [[https://dev.gnupg.org/source/gpgme/browse/ben%252Fhowto-dita/][an alternative branch]] of the GPGME +git repository. + +These draft editions are not official documents and the version of +documentation in the master branch or which ships with released +versions is the only official documentation. Nevertheless, these +draft editions may occasionally be of use by providing more accessible +web versions which are updated between releases. They are provided on +the understanding that they may contain errors or may contain content +subject to change prior to an official release. + + +** License GPL compatible + :PROPERTIES: + :CUSTOM_ID: license + :END: + +This file is free software; as a special exception the author gives +unlimited permission to copy and/or distribute it, with or without +modifications, as long as this notice is preserved. + +This file is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY, to the extent permitted by law; without even the +implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +PURPOSE. + + +* Footnotes + +[fn:1] =short-history.org= and/or =short-history.html=. + +[fn:2] With no issues reported specific to Python 3.7, the release of +Python 3.7.1 at around the same time as GPGME 1.12.0 and the testing +with Python 3.7.1rc1, there is no reason to delay moving 3.7 ahead of +3.6 now. Production environments with more conservative requirements +will always enforce their own policies anyway and installation to each +supported minor release is quite possible too. + +[fn:3] Yes, even if you use virtualenv with everything you do in +Python. If you want to install this module as just your user account +then you will need to manually configure, compile and install the +/entire/ GnuPG stack as that user as well. This includes libraries +which are not often installed that way. It can be done and there are +circumstances under which it is worthwhile, but generally only on +POSIX systems which utilise single user mode (some even require it). + +[fn:4] You probably don't really want to do this. Searching the +keyservers for "gnupg.org" produces over 400 results, the majority of +which aren't actually at the gnupg.org domain, but just included a +comment regarding the project in their key somewhere. + +[fn:5] Such as with ProtonMail servers. This also means that +restricted servers which only advertise either HTTP or HTTPS end +points and not HKP or HKPS end points must still be identified as as +HKP or HKPS within the Python Code. The =hkp4py= module will rewrite +these appropriately when the connection is made to the server. diff --git a/lang/python/doc/src/index.org b/lang/python/doc/src/index.org new file mode 100644 index 0000000..701d986 --- /dev/null +++ b/lang/python/doc/src/index.org @@ -0,0 +1,25 @@ +#+TITLE: GNU Privacy Guard (GnuPG) Made Easy Python Bindings +#+AUTHOR: Ben McGinnes +#+LATEX_COMPILER: xelatex +#+LATEX_CLASS: article +#+LATEX_CLASS_OPTIONS: [12pt] +#+LATEX_HEADER: \usepackage{xltxtra} +#+LATEX_HEADER: \usepackage[margin=1in]{geometry} +#+LATEX_HEADER: \setmainfont[Ligatures={Common}]{Times New Roman} +#+LATEX_HEADER: \author{Ben McGinnes <ben@gnupg.org>} + + +* GPGME Python Bindings + :PROPERTIES: + :CUSTOM_ID: index + :END: + + +** Contents + :PROPERTIES: + :CUSTOM_ID: index-contents + :END: + + +- [[file:short-history.org][A short history of the project]] +- [[file:gpgme-python-howto.org][GPGME Python Bindings HOWTO]] diff --git a/lang/python/doc/src/short-history.org b/lang/python/doc/src/short-history.org new file mode 100644 index 0000000..587cb9f --- /dev/null +++ b/lang/python/doc/src/short-history.org @@ -0,0 +1,172 @@ +#+TITLE: A Short History of the GPGME bindings for Python +#+LATEX_COMPILER: xelatex +#+LATEX_CLASS: article +#+LATEX_CLASS_OPTIONS: [12pt] +#+LATEX_HEADER: \usepackage{xltxtra} +#+LATEX_HEADER: \usepackage[margin=1in]{geometry} +#+LATEX_HEADER: \setmainfont[Ligatures={Common}]{Latin Modern Roman} + +* Overview + :PROPERTIES: + :CUSTOM_ID: overview + :END: + +The GPGME Python bindings passed through many hands and numerous +phases before, after a fifteen year journey, coming full circle to +return to the source. This is a short explanation of that journey. + +** In the beginning + :PROPERTIES: + :CUSTOM_ID: in-the-begining + :END: + + In 2002 John Goerzen released PyME; Python bindings for the GPGME + module which utilised the current release of Python of the time and + SWIG.[fn:1] Shortly after creating it and ensuring it worked he stopped + supporting it, though he left his work available on his Gopher + site. + +** Keeping the flame alive + :PROPERTIES: + :CUSTOM_ID: keeping-the-flame-alive + :END: + + A couple of years later the project was picked up by Igor Belyi and + actively developed and maintained by him from 2004 to 2008. Igor's + whereabouts at the time of this document's creation are unknown, + but the current authors do hope he is well. We're assuming (or + hoping) that life did what life does and made continuing untenable. + +** Passing the torch + :PROPERTIES: + :CUSTOM_ID: passing-the-torch + :END: + + In 2014 Martin Albrecht wanted to patch a bug in the PyME code and + discovered the absence of Igor. Following a discussion on the PyME + mailing list he became the new maintainer for PyME, releasing + version 0.9.0 in May of that year. He remains the maintainer of + the original PyME release in Python 2.6 and 2.7 (available via + PyPI). + +** Coming full circle + :PROPERTIES: + :CUSTOM_ID: ouroboros + :END: + + In 2015 Ben McGinnes approached Martin about a Python 3 version, + while investigating how complex a task this would be the task ended + up being completed. A subsequent discussion with Werner Koch led + to the decision to fold the Python 3 port back into the original + GPGME release in the languages subdirectory for non-C bindings + under the module name of =pyme3=. + + In 2016 this PyME module was integrated back into the GPGME project + by Justus Winter. During the course of this work Justus adjusted + the port to restore limited support for Python 2, but not as many + minor point releases as the original PyME package supports. During + the course of this integration the package was renamed to more + accurately reflect its status as a component of GPGME. The =pyme3= + module was renamed to =gpg= and adopted by the upstream GnuPG team. + + In 2017 Justus departed G10code and the GnuPG team. Following this + Ben returned to maintain of gpgme Python bindings and continue + building them from that point. + +* Relics of the past + :PROPERTIES: + :CUSTOM_ID: relics-past + :END: + +There are a few things, in addition to code specific factors, such as +SWIG itself, which are worth noting here. + +** The Annoyances of Git + :PROPERTIES: + :CUSTOM_ID: the-annoyances-of-git + :END: + + As anyone who has ever worked with git knows, submodules are + horrible way to deal with pretty much anything. In the interests + of avoiding migraines, that was skipped with addition of the PyME + code to GPGME. + + Instead the files were added to a subdirectory of the =lang/= + directory, along with a copy of the entire git log up to that point + as a separate file within the =lang/python/docs/= directory.[fn:2] + As the log for PyME is nearly 100KB and the log for GPGME is + approximately 1MB, this would cause considerable bloat, as well as + some confusion, should the two be merged. + + Hence the unfortunate, but necessary, step to simply move the + files. A regular repository version has been maintained should it + be possible to implement this better in the future. + +** The Perils of PyPI + :PROPERTIES: + :CUSTOM_ID: the-perils-of-pypi + :END: + + The early port of the Python 2 =pyme= module as =pyme3= was never + added to PyPI while the focus remained on development and testing + during 2015 and early 2016. Later in 2016, however, when Justus + completed his major integration work and subsequently renamed the + module from =pyme3= to =gpg=, some prior releases were also + provided through PyPI. + + Since these bindings require a matching release of the GPGME + libraries in order to function, it was determined that there was + little benefit in also providing a copy through PyPI since anyone + obtaining the GPGME source code would obtain the Python bindings + source code at the same time. Whereas there was the potential to + sew confusion amongst Python users installing the module from PyPI, + only to discover that without the relevant C files, header files or + SWIG compiled binaries, the Python module did them little good. + + There are only two files on PyPI which might turn up in a search + for this module or a sample of its content: + + 1. gpg (1.8.0) - Python bindings for GPGME GnuPG cryptography library + 2. pyme (0.9.0) - Python support for GPGME GnuPG cryptography library + +*** GPG 1·8·0 - Python bindings for GPGME GnuPG cryptography library + :PROPERTIES: + :CUSTOM_ID: pypi-gpgme-180 + :END: + + This is the most recent version to reach PyPI and is the version + of the official Pyhon bindings which shipped with GPGME 1.8.0. If + you have GPGME 1.8.0 installed and /only/ 1.8.0 installed, then it + is probably safe to use this copy from PyPI. + + As there have been a lot of changes since the release of GPGME + 1.8.0, the GnuPG Project recommends not using this version of the + module and instead installing the current version of GPGME along + with the Python bindings included with that package. + +*** PyME 0·9·0 - Python support for GPGME GnuPG cryptography library + :PROPERTIES: + :CUSTOM_ID: pypi-gpgme-90 + :END: + + This is the last release of the PyME bindings maintained by Martin + Albrecht and is only compatible with Python 2, it will not work + with Python 3. This is the version of the software from which the + port from Python 2 to Python 3 code was made in 2015. + + Users of the more recent Python bindings will recognise numerous + points of similarity, but also significant differences. It is + likely that the more recent official bindings will feel "more + pythonic." + + For those using Python 2, there is essentially no harm in using + this module, but it may lack a number of more recent features + added to GPGME. + +* Footnotes + +[fn:1] In all likelihood thos would have been Python 2.2 or possibly +Python 2.3. + +[fn:2] The entire PyME git log and other preceding VCS logs are +located in the =gpgme/lang/python/docs/old-commits.log= file. diff --git a/lang/python/doc/texinfo/gpgme-python-howto.texi b/lang/python/doc/texinfo/gpgme-python-howto.texi new file mode 100644 index 0000000..40beb7a --- /dev/null +++ b/lang/python/doc/texinfo/gpgme-python-howto.texi @@ -0,0 +1,3155 @@ +\input texinfo @c -*- texinfo -*- +@c %**start of header +@setfilename gpgme-python-howto.info +@settitle GNU Privacy Guard (GnuPG) Made Easy Python Bindings HOWTO (English) +@documentencoding UTF-8 +@documentlanguage en +@c %**end of header + +@finalout +@titlepage +@title GNU Privacy Guard (GnuPG) Made Easy Python Bindings HOWTO (English) +@author Ben McGinnes +@end titlepage + +@contents + +@ifnottex +@node Top +@top GNU Privacy Guard (GnuPG) Made Easy Python Bindings HOWTO (English) +@end ifnottex + +@menu +* Introduction:: +* GPGME Concepts:: +* GPGME Python bindings installation:: +* Fundamentals:: +* Working with keys:: +* Basic Functions:: +* Creating keys and subkeys:: +* Advanced or Experimental Use Cases:: +* Miscellaneous extras and work-arounds:: +* Copyright and Licensing:: + +@detailmenu +--- The Detailed Node Listing --- + +Introduction + +* Python 2 versus Python 3:: +* Examples:: +* Unofficial Drafts:: +* What's New:: + +What's New + +* New in GPGME 1·12·0:: + +GPGME Concepts + +* A C API:: +* Python bindings:: +* Difference between the Python bindings and other GnuPG Python packages:: + +Difference between the Python bindings and other GnuPG Python packages + +* The python-gnupg package maintained by Vinay Sajip:: +* The gnupg package created and maintained by Isis Lovecruft:: +* The PyME package maintained by Martin Albrecht:: + +GPGME Python bindings installation + +* No PyPI:: +* Requirements:: +* Installation:: +* Known Issues:: + +Requirements + +* Recommended Additions:: + +Installation + +* Installing GPGME:: + +Known Issues + +* Breaking Builds:: +* Reinstalling Responsibly:: +* Multiple installations:: +* Won't Work With Windows:: +* CFFI is the Best™ and GPGME should use it instead of SWIG:: +* Virtualised Environments:: + +Fundamentals + +* No REST:: +* Context:: + +Working with keys + +* Key selection:: +* Get key:: +* Importing keys:: +* Exporting keys:: + +Key selection + +* Counting keys:: + +Importing keys + +* Working with ProtonMail:: +* Importing with HKP for Python:: +* Importing from ProtonMail with HKP for Python:: + +Exporting keys + +* Exporting public keys:: +* Exporting secret keys:: +* Sending public keys to the SKS Keyservers:: + +Basic Functions + +* Encryption:: +* Decryption:: +* Signing text and files:: +* Signature verification:: + +Encryption + +* Encrypting to one key:: +* Encrypting to multiple keys:: + +Signing text and files + +* Signing key selection:: +* Normal or default signing messages or files:: +* Detached signing messages and files:: +* Clearsigning messages or text:: + +Creating keys and subkeys + +* Primary key:: +* Subkeys:: +* User IDs:: +* Key certification:: + +User IDs + +* Adding User IDs:: +* Revokinging User IDs:: + +Advanced or Experimental Use Cases + +* C plus Python plus SWIG plus Cython:: + +Miscellaneous extras and work-arounds + +* Group lines:: +* Keyserver access for Python:: + +Keyserver access for Python + +* Key import format:: + +Copyright and Licensing + +* Copyright:: +* Draft Editions of this HOWTO:: +* License GPL compatible:: + +@end detailmenu +@end menu + +@node Introduction +@chapter Introduction + +@multitable {aaaaaaaaaaaaaaa} {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} +@item Version: +@tab 0.1.4 +@item GPGME Version: +@tab 1.12.0 +@item Author: +@tab @uref{https://gnupg.org/people/index.html#sec-1-5, Ben McGinnes} <ben@@gnupg.org> +@item Author GPG Key: +@tab DB4724E6FA4286C92B4E55C4321E4E2373590E5D +@item Language: +@tab Australian English, British English +@item xml:lang: +@tab en-AU, en-GB, en +@end multitable + +This document provides basic instruction in how to use the GPGME +Python bindings to programmatically leverage the GPGME library. + +@menu +* Python 2 versus Python 3:: +* Examples:: +* Unofficial Drafts:: +* What's New:: +@end menu + +@node Python 2 versus Python 3 +@section Python 2 versus Python 3 + +Though the GPGME Python bindings themselves provide support for both +Python 2 and 3, the focus is unequivocally on Python 3 and +specifically from Python 3.4 and above. As a consequence all the +examples and instructions in this guide use Python 3 code. + +Much of it will work with Python 2, but much of it also deals with +Python 3 byte literals, particularly when reading and writing data. +Developers concentrating on Python 2.7, and possibly even 2.6, will +need to make the appropriate modifications to support the older string +and unicode types as opposed to bytes. + +There are multiple reasons for concentrating on Python 3; some of +which relate to the immediate integration of these bindings, some of +which relate to longer term plans for both GPGME and the python +bindings and some of which relate to the impending EOL period for +Python 2.7. Essentially, though, there is little value in tying the +bindings to a version of the language which is a dead end and the +advantages offered by Python 3 over Python 2 make handling the data +types with which GPGME deals considerably easier. + +@node Examples +@section Examples + +All of the examples found in this document can be found as Python 3 +scripts in the @samp{lang/python/examples/howto} directory. + +@node Unofficial Drafts +@section Unofficial Drafts + +In addition to shipping with each release of GPGME, there is a section +on locations to read or download @ref{Draft Editions of this HOWTO, , draft editions} of this document from +at the end of it. These are unofficial versions produced in between +major releases. + +@node What's New +@section What's New + +The most obviously new point for those reading this guide is this +section on other new things, but that's hardly important. Not given +all the other things which spurred the need for adding this section +and its subsections. + +@menu +* New in GPGME 1·12·0:: +@end menu + +@node New in GPGME 1·12·0 +@subsection New in GPGME 1·12·0 + +There have been quite a number of additions to GPGME and the Python +bindings to it since the last release of GPGME with versions 1.11.0 +and 1.11.1 in April, 2018. + +The bullet points of new additiions are: + +@itemize +@item +an expanded section on @ref{Installation, , installing} and @ref{Known Issues, , troubleshooting} the Python +bindings. +@item +The release of Python 3.7.0; which appears to be working just fine +with our bindings, in spite of intermittent reports of problems for +many other Python projects with that new release. +@item +Python 3.7 has been moved to the head of the specified python +versions list in the build process. +@item +In order to fix some other issues, there are certain underlying +functions which are more exposed through the @ref{Context, , gpg.Context()}, but +ongoing documentation ought to clarify that or otherwise provide the +best means of using the bindings. Some additions to @samp{gpg.core} and +the @samp{Context()}, however, were intended (see below). +@item +Continuing work in identifying and confirming the cause of +oft-reported @ref{Won't Work With Windows, , problems installing the Python bindings on Windows}. +@item +GSOC: Google's Surreptitiously Ordered Conscription @dots{} erm @dots{} oh, +right; Google's Summer of Code. Though there were two hopeful +candidates this year; only one ended up involved with the GnuPG +Project directly, the other concentrated on an unrelated third party +project with closer ties to one of the GNU/Linux distributions than +to the GnuPG Project. Thus the Python bindings benefited from GSOC +participant Jacob Adams, who added the key@math{_import} function; building +on prior work by Tobias Mueller. +@item +Several new methods functions were added to the gpg.Context(), +including: @ref{Importing keys, , key@math{_import}}, @ref{Exporting keys, , key@math{_export}}, @ref{Exporting public keys, , key@math{_export}@math{_minimal}} and +@ref{Exporting secret keys, , key@math{_export}@math{_secret}}. +@item +Importing and exporting examples include versions integrated with +Marcel Fest's recently released @uref{https://github.com/Selfnet/hkp4py, HKP for Python} module. Some +@ref{Keyserver access for Python, , additional notes on this module} are included at the end of the HOWTO. +@item +Instructions for dealing with semi-walled garden implementations +like ProtonMail are also included. This is intended to make things +a little easier when communicating with users of ProtonMail's +services and should not be construed as an endorsement of said +service. The GnuPG Project neither favours, nor disfavours +ProtonMail and the majority of this deals with interacting with the +ProtonMail keyserver. +@item +Semi-formalised the location where @ref{Draft Editions of this HOWTO, , draft versions} of this HOWTO may +periodically be accessible. This is both for the reference of +others and testing the publishing of the document itself. Renamed +this file at around the same time. +@item +The Texinfo documentation build configuration has been replicated +from the parent project in order to make to maintain consistency +with that project (and actually ship with each release). +@item +a reStructuredText (@samp{.rst}) version is also generated for Python +developers more used to and comfortable with that format as it is +the standard Python documentation format and Python developers may +wish to use it with Sphinx. Please note that there has been no +testing of the reStructuredText version with Sphinx at all. The +reST file was generated by the simple expedient of using @uref{https://pandoc.org/, Pandoc}. +@item +Added a new section for @ref{Advanced or Experimental Use Cases, , advanced or experimental use}. +@item +Began the advanced use cases with @ref{C plus Python plus SWIG plus Cython, , a section} on using the module with +@uref{http://cython.org/, Cython}. +@item +Added a number of new scripts to the @samp{example/howto/} directory; +some of which may be in advance of their planned sections of the +HOWTO (and some are just there because it seemed like a good idea at +the time). +@item +Cleaned up a lot of things under the hood. +@end itemize + +@node GPGME Concepts +@chapter GPGME Concepts + +@menu +* A C API:: +* Python bindings:: +* Difference between the Python bindings and other GnuPG Python packages:: +@end menu + +@node A C API +@section A C API + +Unlike many modern APIs with which programmers will be more familiar +with these days, the GPGME API is a C API. The API is intended for +use by C coders who would be able to access its features by including +the @samp{gpgme.h} header file with their own C source code and then access +its functions just as they would any other C headers. + +This is a very effective method of gaining complete access to the API +and in the most efficient manner possible. It does, however, have the +drawback that it cannot be directly used by other languages without +some means of providing an interface to those languages. This is +where the need for bindings in various languages stems. + +@node Python bindings +@section Python bindings + +The Python bindings for GPGME provide a higher level means of +accessing the complete feature set of GPGME itself. It also provides +a more pythonic means of calling these API functions. + +The bindings are generated dynamically with SWIG and the copy of +@samp{gpgme.h} generated when GPGME is compiled. + +This means that a version of the Python bindings is fundamentally tied +to the exact same version of GPGME used to generate that copy of +@samp{gpgme.h}. + +@node Difference between the Python bindings and other GnuPG Python packages +@section Difference between the Python bindings and other GnuPG Python packages + +There have been numerous attempts to add GnuPG support to Python over +the years. Some of the most well known are listed here, along with +what differentiates them. + +@menu +* The python-gnupg package maintained by Vinay Sajip:: +* The gnupg package created and maintained by Isis Lovecruft:: +* The PyME package maintained by Martin Albrecht:: +@end menu + +@node The python-gnupg package maintained by Vinay Sajip +@subsection The python-gnupg package maintained by Vinay Sajip + +This is arguably the most popular means of integrating GPG with +Python. The package utilises the @samp{subprocess} module to implement +wrappers for the @samp{gpg} and @samp{gpg2} executables normally invoked on the +command line (@samp{gpg.exe} and @samp{gpg2.exe} on Windows). + +The popularity of this package stemmed from its ease of use and +capability in providing the most commonly required features. + +Unfortunately it has been beset by a number of security issues in the +past; most of which stemmed from using unsafe methods of accessing the +command line via the @samp{subprocess} calls. While some effort has been +made over the last two to three years (as of 2018) to mitigate this, +particularly by no longer providing shell access through those +subprocess calls, the wrapper is still somewhat limited in the scope +of its GnuPG features coverage. + +The python-gnupg package is available under the MIT license. + +@node The gnupg package created and maintained by Isis Lovecruft +@subsection The gnupg package created and maintained by Isis Lovecruft + +In 2015 Isis Lovecruft from the Tor Project forked and then +re-implemented the python-gnupg package as just gnupg. This new +package also relied on subprocess to call the @samp{gpg} or @samp{gpg2} +binaries, but did so somewhat more securely. + +The naming and version numbering selected for this package, however, +resulted in conflicts with the original python-gnupg and since its +functions were called in a different manner to python-gnupg, the +release of this package also resulted in a great deal of consternation +when people installed what they thought was an upgrade that +subsequently broke the code relying on it. + +The gnupg package is available under the GNU General Public License +version 3.0 (or any later version). + +@node The PyME package maintained by Martin Albrecht +@subsection The PyME package maintained by Martin Albrecht + +This package is the origin of these bindings, though they are somewhat +different now. For details of when and how the PyME package was +folded back into GPGME itself see the @uref{short-history.org, Short History} document.@footnote{@samp{short-history.org} and/or @samp{short-history.html}.} + +The PyME package was first released in 2002 and was also the first +attempt to implement a low level binding to GPGME. In doing so it +provided access to considerably more functionality than either the +@samp{python-gnupg} or @samp{gnupg} packages. + +The PyME package is only available for Python 2.6 and 2.7. + +Porting the PyME package to Python 3.4 in 2015 is what resulted in it +being folded into the GPGME project and the current bindings are the +end result of that effort. + +The PyME package is available under the same dual licensing as GPGME +itself: the GNU General Public License version 2.0 (or any later +version) and the GNU Lesser General Public License version 2.1 (or any +later version). + +@node GPGME Python bindings installation +@chapter GPGME Python bindings installation + +@menu +* No PyPI:: +* Requirements:: +* Installation:: +* Known Issues:: +@end menu + +@node No PyPI +@section No PyPI + +Most third-party Python packages and modules are available and +distributed through the Python Package Installer, known as PyPI. + +Due to the nature of what these bindings are and how they work, it is +infeasible to install the GPGME Python bindings in the same way. + +This is because the bindings use SWIG to dynamically generate C +bindings against @samp{gpgme.h} and @samp{gpgme.h} is generated from +@samp{gpgme.h.in} at compile time when GPGME is built from source. Thus to +include a package in PyPI which actually built correctly would require +either statically built libraries for every architecture bundled with +it or a full implementation of C for each architecture. + +See the additional notes regarding @ref{CFFI is the Best™ and GPGME should use it instead of SWIG, , CFFI and SWIG} at the end of this +section for further details. + +@node Requirements +@section Requirements + +The GPGME Python bindings only have three requirements: + +@enumerate +@item +A suitable version of Python 2 or Python 3. With Python 2 that +means CPython 2.7 and with Python 3 that means CPython 3.4 or +higher. +@item +@uref{https://www.swig.org, SWIG}. +@item +GPGME itself. Which also means that all of GPGME's dependencies +must be installed too. +@end enumerate + +@menu +* Recommended Additions:: +@end menu + +@node Recommended Additions +@subsection Recommended Additions + +Though none of the following are absolute requirements, they are all +recommended for use with the Python bindings. In some cases these +recommendations refer to which version(s) of CPython to use the +bindings with, while others refer to third party modules which provide +a significant advantage in some way. + +@enumerate +@item +If possible, use Python 3 instead of 2. +@item +Favour a more recent version of Python since even 3.4 is due to +reach EOL soon. In production systems and services, Python 3.6 +should be robust enough to be relied on. +@item +If possible add the following Python modules which are not part of +the standard library: @uref{http://docs.python-requests.org/en/latest/index.html, Requests}, @uref{http://cython.org/, Cython} and @uref{https://github.com/Selfnet/hkp4py, hkp4py}. Chances are +quite high that at least the first one and maybe two of those will +already be installed. +@end enumerate + +Note that, as with Cython, some of the planned additions to the +@ref{Advanced or Experimental Use Cases, , Advanced} section, will bring with them additional requirements. Most +of these will be fairly well known and commonly installed ones, +however, which are in many cases likely to have already been installed +on many systems or be familiar to Python programmers. + +@node Installation +@section Installation + +Installing the Python bindings is effectively achieved by compiling +and installing GPGME itself. + +Once SWIG is installed with Python and all the dependencies for GPGME +are installed you only need to confirm that the version(s) of Python +you want the bindings installed for are in your @samp{$PATH}. + +By default GPGME will attempt to install the bindings for the most +recent or highest version number of Python 2 and Python 3 it detects +in @samp{$PATH}. It specifically checks for the @samp{python} and @samp{python3} +executables first and then checks for specific version numbers. + +For Python 2 it checks for these executables in this order: @samp{python}, +@samp{python2} and @samp{python2.7}. + +For Python 3 it checks for these executables in this order: @samp{python3}, + @samp{python3.7}, @samp{python3.6}, @samp{python3.5} and @samp{python3.4}.@footnote{With no issues reported specific to Python 3.7, the release of +Python 3.7.1 at around the same time as GPGME 1.12.0 and the testing +with Python 3.7.1rc1, there is no reason to delay moving 3.7 ahead of +3.6 now. Production environments with more conservative requirements +will always enforce their own policies anyway and installation to each +supported minor release is quite possible too.} + +On systems where @samp{python} is actually @samp{python3} and not @samp{python2} it +may be possible that @samp{python2} may be overlooked, but there have been +no reports of that actually occurring as yet. + +In the three months or so since the release of Python 3.7.0 there has +been extensive testing and work with these bindings with no issues +specifically relating to the new version of Python or any of the new +features of either the language or the bindings. This has also been +the case with Python 3.7.1rc1. With that in mind and given the +release of Python 3.7.1 is scheduled for around the same time as GPGME +1.12.0, the order of preferred Python versions has been changed to +move Python 3.7 ahead of Python 3.6. + +@menu +* Installing GPGME:: +@end menu + +@node Installing GPGME +@subsection Installing GPGME + +See the GPGME @samp{README} file for details of how to install GPGME from +source. + +@node Known Issues +@section Known Issues + +There are a few known issues with the current build process and the +Python bindings. For the most part these are easily addressed should +they be encountered. + +@menu +* Breaking Builds:: +* Reinstalling Responsibly:: +* Multiple installations:: +* Won't Work With Windows:: +* CFFI is the Best™ and GPGME should use it instead of SWIG:: +* Virtualised Environments:: +@end menu + +@node Breaking Builds +@subsection Breaking Builds + +Occasionally when installing GPGME with the Python bindings included +it may be observed that the @samp{make} portion of that process induces a +large very number of warnings and, eventually errors which end that +part of the build process. Yet following that with @samp{make check} and +@samp{make install} appears to work seamlessly. + +The cause of this is related to the way SWIG needs to be called to +dynamically generate the C bindings for GPGME in the first place. So +the entire process will always produce @samp{lang/python/python2-gpg/} and +@samp{lang/python/python3-gpg/} directories. These should contain the +build output generated during compilation, including the complete +bindings and module installed into @samp{site-packages}. + +Occasionally the errors in the early part or some other conflict +(e.g. not installing as @strong{@emph{root}} or @strong{@emph{su}}) may result in nothing +being installed to the relevant @samp{site-packages} directory and the +build directory missing a lot of expected files. Even when this +occurs, the solution is actually quite simple and will always work. + +That solution is simply to run the following commands as either the +@strong{root} user or prepended with @samp{sudo -H}@footnote{Yes, even if you use virtualenv with everything you do in +Python. If you want to install this module as just your user account +then you will need to manually configure, compile and install the +@emph{entire} GnuPG stack as that user as well. This includes libraries +which are not often installed that way. It can be done and there are +circumstances under which it is worthwhile, but generally only on +POSIX systems which utilise single user mode (some even require it).} in the @samp{lang/python/} +directory: + +@example +/path/to/pythonX.Y setup.py build +/path/to/pythonX.Y setup.py build +/path/to/pythonX.Y setup.py install +@end example + +Yes, the build command does need to be run twice. Yes, you still need +to run the potentially failing or incomplete steps during the +@samp{configure}, @samp{make} and @samp{make install} steps with installing GPGME. +This is because those steps generate a lot of essential files needed, +both by and in order to create, the bindings (including both the +@samp{setup.py} and @samp{gpgme.h} files). + +@enumerate +@item +IMPORTANT Note + + +If specifying a selected number of languages to create bindings for, +try to leave Python last. Currently the majority of the other +language bindings are also preceding Python of either version when +listed alphabetically and so that just happens by default currently. + +If Python is set to precede one of the other languages then it is +possible that the errors described here may interrupt the build +process before generating bindings for those other languages. In +these cases it may be preferable to configure all preferred language +bindings separately with alternative @samp{configure} steps for GPGME using +the @samp{--enable-languages=$LANGUAGE} option. +@end enumerate + +@node Reinstalling Responsibly +@subsection Reinstalling Responsibly + +Regardless of whether you're installing for one version of Python or +several, there will come a point where reinstallation is required. +With most Python module installations, the installed files go into the +relevant site-packages directory and are then forgotten about. Then +the module is upgraded, the new files are copied over the old and +that's the end of the matter. + +While the same is true of these bindings, there have been intermittent +issues observed on some platforms which have benefited significantly +from removing all the previous installations of the bindings before +installing the updated versions. + +Removing the previous version(s) is simply a matter of changing to the +relevant @samp{site-packages} directory for the version of Python in +question and removing the @samp{gpg/} directory and any accompanying +egg-info files for that module. + +In most cases this will require root or administration privileges on +the system, but the same is true of installing the module in the first +place. + +@node Multiple installations +@subsection Multiple installations + +For a veriety of reasons it may be either necessary or just preferable +to install the bindings to alternative installed Python versions which +meet the requirements of these bindings. + +On POSIX systems this will generally be most simply achieved by +running the manual installation commands (build, build, install) as +described in the previous section for each Python installation the +bindings need to be installed to. + +As per the SWIG documentation: the compilers, libraries and runtime +used to build GPGME and the Python Bindings @strong{must} match those used to +compile Python itself, including the version number(s) (at least going +by major version numbers and probably minor numbers too). + +On most POSIX systems, including OS X, this will very likely be the +case in most, if not all, cases. + +@node Won't Work With Windows +@subsection Won't Work With Windows + +There are semi-regular reports of Windows users having considerable +difficulty in installing and using the Python bindings at all. Very +often, possibly even always, these reports come from Cygwin users +and/or MinGW users and/or Msys2 users. Though not all of them have +been confirmed, it appears that these reports have also come from +people who installed Python using the Windows installer files from the +@uref{https://python.org, Python website} (i.e. mostly MSI installers, sometimes self-extracting +@samp{.exe} files). + +The Windows versions of Python are not built using Cygwin, MinGW or +Msys2; they're built using Microsoft Visual Studio. Furthermore the +version used is @emph{considerably} more advanced than the version which +MinGW obtained a small number of files from many years ago in order to +be able to compile anything at all. Not only that, but there are +changes to the version of Visual Studio between some micro releases, +though that is is particularly the case with Python 2.7, since it has +been kept around far longer than it should have been. + +There are two theoretical solutions to this issue: + +@enumerate +@item +Compile and install the GnuPG stack, including GPGME and the +Python bibdings using the same version of Microsoft Visual Studio +used by the Python Foundation to compile the version of Python +installed. + +If there are multiple versions of Python then this will need to be +done with each different version of Visual Studio used. + +@item +Compile and install Python using the same tools used by choice, +such as MinGW or Msys2. +@end enumerate + +Do @strong{not} use the official Windows installer for Python unless +following the first method. + +In this type of situation it may even be for the best to accept that +there are less limitations on permissive software than free software +and simply opt to use a recent version of the Community Edition of +Microsoft Visual Studio to compile and build all of it, no matter +what. + +Investigations into the extent or the limitations of this issue are +ongoing. + +@node CFFI is the Best™ and GPGME should use it instead of SWIG +@subsection CFFI is the Best™ and GPGME should use it instead of SWIG + +There are many reasons for favouring @uref{https://cffi.readthedocs.io/en/latest/overview.html, CFFI} and proponents of it are +quite happy to repeat these things as if all it would take to switch +from SWIG to CFFI is repeating that list as if it were a new concept. + +The fact is that there are things which Python's CFFI implementation +cannot handle in the GPGME C code. Beyond that there are features of +SWIG which are simply not available with CFFI at all. SWIG generates +the bindings to Python using the @samp{gpgme.h} file, but that file is not +a single version shipped with each release, it too is generated when +GPGME is compiled. + +CFFI is currently unable to adapt to such a potentially mutable +codebase. If there were some means of applying SWIG's dynamic code +generation to produce the Python/CFFI API modes of accessing the GPGME +libraries (or the source source code directly), but such a thing does +not exist yet either and it currently appears that work is needed in +at least one of CFFI's dependencies before any of this can be +addressed. + +So if you're a massive fan of CFFI; that's great, but if you want this +project to switch to CFFI then rather than just insisting that it +should, I'd suggest you volunteer to bring CFFI up to the level this +project needs. + +If you're actually seriously considering doing so, then I'd suggest +taking the @samp{gpgme-tool.c} file in the GPGME @samp{src/} directory and +getting that to work with any of the CFFI API methods (not the ABI +methods, they'll work with pretty much anything). When you start +running into trouble with "ifdefs" then you'll know what sort of +things are lacking. That doesn't even take into account the amount of +work saved via SWIG's code generation techniques either. + +@node Virtualised Environments +@subsection Virtualised Environments + +It is fairly common practice amongst Python developers to, as much as +possible, use packages like virtualenv to keep various things that are +to be installed from interfering with each other. Given how much of +the GPGME bindings is often at odds with the usual pythonic way of +doing things, it stands to reason that this would be called into +question too. + +As it happens the answer as to whether or not the bindings can be used +with virtualenv, the answer is both yes and no. + +In general we recommend installing to the relevant path and matching +prefix of GPGME itself. Which means that when GPGME, and ideally the +rest of the GnuPG stack, is installed to a prefix like @samp{/usr/local} or +@samp{/opt/local} then the bindings would need to be installed to the main +Python installation and not a virtualised abstraction. Attempts to +separate the two in the past have been known to cause weird and +intermittent errors ranging from minor annoyances to complete failures +in the build process. + +As a consequence we only recommend building with and installing to the +main Python installations within the same prefix as GPGME is installed +to or which are found by GPGME's configuration stage immediately prior +to running the make commands. Which is exactly what the compiling and +installing process of GPGME does by default. + +Once that is done, however, it appears that a copy the compiled module +may be installed into a virtualenv of the same major and minor version +matching the build. Alternatively it is possible to utilise a +@samp{sites.pth} file in the @samp{site-packages/} directory of a viertualenv +installation, which links back to the system installations +corresponding directory in order to import anything installed system +wide. This may or may not be appropriate on a case by case basis. + +Though extensive testing of either of these options is not yet +complete, preliminary testing of them indicates that both are viable +as long as the main installation is complete. Which means that +certain other options normally restricted to virtual environments are +also available, including integration with pythonic test suites +(e.g. @uref{https://docs.pytest.org/en/latest/index.html, pytest}) and other large projects. + +That said, it is worth reiterating the warning regarding non-standard +installations. If one were to attempt to install the bindings only to +a virtual environment without somehow also including the full GnuPG +stack (or enough of it as to include GPGME) then it is highly likely +that errors would be encountered at some point and more than a little +likely that the build process itself would break. + +If a degree of separation from the main operating system is still +required in spite of these warnings, then consider other forms of +virtualisation. Either a virtual machine (e.g. @uref{https://www.virtualbox.org/, VirtualBox}), a +hardware emulation layer (e.g. @uref{https://www.qemu.org/, QEMU}) or an application container +(e.g. @uref{https://www.docker.com/why-docker, Docker}). + +Finally it should be noted that the limited tests conducted thus far +have been using the @samp{virtualenv} command in a new directory to create +the virtual python environment. As opposed to the standard @samp{python3 +-m venv} and it is possible that this will make a difference depending +on the system and version of Python in use. Another option is to run +the command @samp{python3 -m virtualenv /path/to/install/virtual/thingy} +instead. + +@node Fundamentals +@chapter Fundamentals + +Before we can get to the fun stuff, there are a few matters regarding +GPGME's design which hold true whether you're dealing with the C code +directly or these Python bindings. + +@menu +* No REST:: +* Context:: +@end menu + +@node No REST +@section No REST + +The first part of which is or will be fairly blatantly obvious upon +viewing the first example, but it's worth reiterating anyway. That +being that this API is @emph{@strong{not}} a REST API. Nor indeed could it ever +be one. + +Most, if not all, Python programmers (and not just Python programmers) +know how easy it is to work with a RESTful API. In fact they've +become so popular that many other APIs attempt to emulate REST-like +behaviour as much as they are able. Right down to the use of JSON +formatted output to facilitate the use of their API without having to +retrain developers. + +This API does not do that. It would not be able to do that and also +provide access to the entire C API on which it's built. It does, +however, provide a very pythonic interface on top of the direct +bindings and it's this pythonic layer that this HOWTO deals with. + +@node Context +@section Context + +One of the reasons which prevents this API from being RESTful is that +most operations require more than one instruction to the API to +perform the task. Sure, there are certain functions which can be +performed simultaneously, particularly if the result known or strongly +anticipated (e.g. selecting and encrypting to a key known to be in the +public keybox). + +There are many more, however, which cannot be manipulated so readily: +they must be performed in a specific sequence and the result of one +operation has a direct bearing on the outcome of subsequent +operations. Not merely by generating an error either. + +When dealing with this type of persistent state on the web, full of +both the RESTful and REST-like, it's most commonly referred to as a +session. In GPGME, however, it is called a context and every +operation type has one. + +@node Working with keys +@chapter Working with keys + +@menu +* Key selection:: +* Get key:: +* Importing keys:: +* Exporting keys:: +@end menu + +@node Key selection +@section Key selection + +Selecting keys to encrypt to or to sign with will be a common +occurrence when working with GPGMe and the means available for doing +so are quite simple. + +They do depend on utilising a Context; however once the data is +recorded in another variable, that Context does not need to be the +same one which subsequent operations are performed. + +The easiest way to select a specific key is by searching for that +key's key ID or fingerprint, preferably the full fingerprint without +any spaces in it. A long key ID will probably be okay, but is not +advised and short key IDs are already a problem with some being +generated to match specific patterns. It does not matter whether the +pattern is upper or lower case. + +So this is the best method: + +@example +import gpg + +k = gpg.Context().keylist(pattern="258E88DCBD3CD44D8E7AB43F6ECB6AF0DEADBEEF") +keys = list(k) +@end example + +This is passable and very likely to be common: + +@example +import gpg + +k = gpg.Context().keylist(pattern="0x6ECB6AF0DEADBEEF") +keys = list(k) +@end example + +And this is a really bad idea: + +@example +import gpg + +k = gpg.Context().keylist(pattern="0xDEADBEEF") +keys = list(k) +@end example + +Alternatively it may be that the intention is to create a list of keys +which all match a particular search string. For instance all the +addresses at a particular domain, like this: + +@example +import gpg + +ncsc = gpg.Context().keylist(pattern="ncsc.mil") +nsa = list(ncsc) +@end example + +@menu +* Counting keys:: +@end menu + +@node Counting keys +@subsection Counting keys + +Counting the number of keys in your public keybox (@samp{pubring.kbx}), the +format which has superseded the old keyring format (@samp{pubring.gpg} and +@samp{secring.gpg}), or the number of secret keys is a very simple task. + +@example +import gpg + +c = gpg.Context() +seckeys = c.keylist(pattern=None, secret=True) +pubkeys = c.keylist(pattern=None, secret=False) + +seclist = list(seckeys) +secnum = len(seclist) + +publist = list(pubkeys) +pubnum = len(publist) + +print(""" + Number of secret keys: @{0@} + Number of public keys: @{1@} +""".format(secnum, pubnum)) +@end example + +NOTE: The @ref{C plus Python plus SWIG plus Cython, , Cython} introduction in the @ref{Advanced or Experimental Use Cases, , Advanced and Experimental} +section uses this same key counting code with Cython to demonstrate +some areas where Cython can improve performance even with the +bindings. Users with large public keyrings or keyboxes, for instance, +should consider these options if they are comfortable with using +Cython. + +@node Get key +@section Get key + +An alternative method of getting a single key via its fingerprint is +available directly within a Context with @samp{Context().get_key}. This is +the preferred method of selecting a key in order to modify it, sign or +certify it and for obtaining relevant data about a single key as a +part of other functions; when verifying a signature made by that key, +for instance. + +By default this method will select public keys, but it can select +secret keys as well. + +This first example demonstrates selecting the current key of Werner +Koch, which is due to expire at the end of 2018: + +@example +import gpg + +fingerprint = "80615870F5BAD690333686D0F2AD85AC1E42B367" +key = gpg.Context().get_key(fingerprint) +@end example + +Whereas this example demonstrates selecting the author's current key +with the @samp{secret} key word argument set to @samp{True}: + +@example +import gpg + +fingerprint = "DB4724E6FA4286C92B4E55C4321E4E2373590E5D" +key = gpg.Context().get_key(fingerprint, secret=True) +@end example + +It is, of course, quite possible to select expired, disabled and +revoked keys with this function, but only to effectively display +information about those keys. + +It is also possible to use both unicode or string literals and byte +literals with the fingerprint when getting a key in this way. + +@node Importing keys +@section Importing keys + +Importing keys is possible with the @samp{key_import()} method and takes +one argument which is a bytes literal object containing either the +binary or ASCII armoured key data for one or more keys. + +The following example retrieves one or more keys from the SKS +keyservers via the web using the requests module. Since requests +returns the content as a bytes literal object, we can then use that +directly to import the resulting data into our keybox. + +@example +import gpg +import os.path +import requests + +c = gpg.Context() +url = "https://sks-keyservers.net/pks/lookup" +pattern = input("Enter the pattern to search for key or user IDs: ") +payload = @{"op": "get", "search": pattern@} + +r = requests.get(url, verify=True, params=payload) +result = c.key_import(r.content) + +if result is not None and hasattr(result, "considered") is False: + print(result) +elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" + The total number of keys considered for import was: @{0@} + + Number of keys revoked: @{1@} + Number of new signatures: @{2@} + Number of new subkeys: @{3@} + Number of new user IDs: @{4@} + Number of new secret keys: @{5@} + Number of unchanged keys: @{6@} + + The key IDs for all considered keys were: +""".format(num_keys, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print("@{0@}\n".format(result.imports[i].fpr)) +else: + pass +@end example + +NOTE: When searching for a key ID of any length or a fingerprint +(without spaces), the SKS servers require the the leading @samp{0x} +indicative of hexadecimal be included. Also note that the old short +key IDs (e.g. @samp{0xDEADBEEF}) should no longer be used due to the +relative ease by which such key IDs can be reproduced, as demonstrated +by the Evil32 Project in 2014 (which was subsequently exploited in +2016). + +@menu +* Working with ProtonMail:: +* Importing with HKP for Python:: +* Importing from ProtonMail with HKP for Python:: +@end menu + +@node Working with ProtonMail +@subsection Working with ProtonMail + +Here is a variation on the example above which checks the constrained +ProtonMail keyserver for ProtonMail public keys. + +@example +import gpg +import requests +import sys + +print(""" +This script searches the ProtonMail key server for the specified key and +imports it. +""") + +c = gpg.Context(armor=True) +url = "https://api.protonmail.ch/pks/lookup" +ksearch = [] + +if len(sys.argv) >= 2: + keyterm = sys.argv[1] +else: + keyterm = input("Enter the key ID, UID or search string: ") + +if keyterm.count("@@") == 2 and keyterm.startswith("@@") is True: + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) +elif keyterm.count("@@") == 1 and keyterm.startswith("@@") is True: + ksearch.append("@{0@}@@protonmail.com".format(keyterm[1:])) + ksearch.append("@{0@}@@protonmail.ch".format(keyterm[1:])) + ksearch.append("@{0@}@@pm.me".format(keyterm[1:])) +elif keyterm.count("@@") == 0: + ksearch.append("@{0@}@@protonmail.com".format(keyterm)) + ksearch.append("@{0@}@@protonmail.ch".format(keyterm)) + ksearch.append("@{0@}@@pm.me".format(keyterm)) +elif keyterm.count("@@") == 2 and keyterm.startswith("@@") is False: + uidlist = keyterm.split("@@") + for uid in uidlist: + ksearch.append("@{0@}@@protonmail.com".format(uid)) + ksearch.append("@{0@}@@protonmail.ch".format(uid)) + ksearch.append("@{0@}@@pm.me".format(uid)) +elif keyterm.count("@@") > 2: + uidlist = keyterm.split("@@") + for uid in uidlist: + ksearch.append("@{0@}@@protonmail.com".format(uid)) + ksearch.append("@{0@}@@protonmail.ch".format(uid)) + ksearch.append("@{0@}@@pm.me".format(uid)) +else: + ksearch.append(keyterm) + +for k in ksearch: + payload = @{"op": "get", "search": k@} + try: + r = requests.get(url, verify=True, params=payload) + if r.ok is True: + result = c.key_import(r.content) + elif r.ok is False: + result = r.content + except Exception as e: + result = None + + if result is not None and hasattr(result, "considered") is False: + print("@{0@} for @{1@}".format(result.decode(), k)) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" +The total number of keys considered for import was: @{0@} + +With UIDs wholely or partially matching the following string: + + @{1@} + + Number of keys revoked: @{2@} + Number of new signatures: @{3@} + Number of new subkeys: @{4@} + Number of new user IDs: @{5@} +Number of new secret keys: @{6@} + Number of unchanged keys: @{7@} + +The key IDs for all considered keys were: +""".format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + elif result is None: + print(e) +@end example + +Both the above example, @uref{../examples/howto/pmkey-import.py, pmkey-import.py}, and a version which prompts +for an alternative GnuPG home directory, @uref{../examples/howto/pmkey-import-alt.py, pmkey-import-alt.py}, are +available with the other examples and are executable scripts. + +Note that while the ProtonMail servers are based on the SKS servers, +their server is related more to their API and is not feature complete +by comparison to the servers in the SKS pool. One notable difference +being that the ProtonMail server does not permit non ProtonMail users +to update their own keys, which could be a vector for attacking +ProtonMail users who may not receive a key's revocation if it had been +compromised. + +@node Importing with HKP for Python +@subsection Importing with HKP for Python + +Performing the same tasks with the @uref{https://github.com/Selfnet/hkp4py, hkp4py module} (available via PyPI) +is not too much different, but does provide a number of options of +benefit to end users. Not least of which being the ability to perform +some checks on a key before importing it or not. For instance it may +be the policy of a site or project to only import keys which have not +been revoked. The hkp4py module permits such checks prior to the +importing of the keys found. + +@example +import gpg +import hkp4py +import sys + +c = gpg.Context() +server = hkp4py.KeyServer("hkps://hkps.pool.sks-keyservers.net") +results = [] + +if len(sys.argv) > 2: + pattern = " ".join(sys.argv[1:]) +elif len(sys.argv) == 2: + pattern = sys.argv[1] +else: + pattern = input("Enter the pattern to search for keys or user IDs: ") + +try: + keys = server.search(pattern) + print("Found @{0@} key(s).".format(len(keys))) +except Exception as e: + keys = [] + for logrus in pattern.split(): + if logrus.startswith("0x") is True: + key = server.search(logrus) + else: + key = server.search("0x@{0@}".format(logrus)) + keys.append(key[0]) + print("Found @{0@} key(s).".format(len(keys))) + +for key in keys: + import_result = c.key_import(key.key_blob) + results.append(import_result) + +for result in results: + if result is not None and hasattr(result, "considered") is False: + print(result) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" +The total number of keys considered for import was: @{0@} + + Number of keys revoked: @{1@} + Number of new signatures: @{2@} + Number of new subkeys: @{3@} + Number of new user IDs: @{4@} +Number of new secret keys: @{5@} + Number of unchanged keys: @{6@} + +The key IDs for all considered keys were: +""".format(num_keys, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + else: + pass +@end example + +Since the hkp4py module handles multiple keys just as effectively as +one (@samp{keys} is a list of responses per matching key), the example +above is able to do a little bit more with the returned data before +anything is actually imported. + +@node Importing from ProtonMail with HKP for Python +@subsection Importing from ProtonMail with HKP for Python + +Though this can provide certain benefits even when working with +ProtonMail, the scope is somewhat constrained there due to the +limitations of the ProtonMail keyserver. + +For instance, searching the SKS keyserver pool for the term "gnupg" +produces hundreds of results from any time the word appears in any +part of a user ID. Performing the same search on the ProtonMail +keyserver returns zero results, even though there are at least two +test accounts which include it as part of the username. + +The cause of this discrepancy is the deliberate configuration of that +server by ProtonMail to require an exact match of the full email +address of the ProtonMail user whose key is being requested. +Presumably this is intended to reduce breaches of privacy of their +users as an email address must already be known before a key for that +address can be obtained. + +@enumerate +@item +Import from ProtonMail via HKP for Python Example no. 1 + + +The following script is avalable with the rest of the examples under +the somewhat less than original name, @samp{pmkey-import-hkp.py}. + +@example +import gpg +import hkp4py +import os.path +import sys + +print(""" +This script searches the ProtonMail key server for the specified key and +imports it. + +Usage: pmkey-import-hkp.py [search strings] +""") + +c = gpg.Context(armor=True) +server = hkp4py.KeyServer("hkps://api.protonmail.ch") +keyterms = [] +ksearch = [] +allkeys = [] +results = [] +paradox = [] +homeless = None + +if len(sys.argv) > 2: + keyterms = sys.argv[1:] +elif len(sys.argv) == 2: + keyterm = sys.argv[1] + keyterms.append(keyterm) +else: + key_term = input("Enter the key ID, UID or search string: ") + keyterms = key_term.split() + +for keyterm in keyterms: + if keyterm.count("@@") == 2 and keyterm.startswith("@@") is True: + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + elif keyterm.count("@@") == 1 and keyterm.startswith("@@") is True: + ksearch.append("@{0@}@@protonmail.com".format(keyterm[1:])) + ksearch.append("@{0@}@@protonmail.ch".format(keyterm[1:])) + ksearch.append("@{0@}@@pm.me".format(keyterm[1:])) + elif keyterm.count("@@") == 0: + ksearch.append("@{0@}@@protonmail.com".format(keyterm)) + ksearch.append("@{0@}@@protonmail.ch".format(keyterm)) + ksearch.append("@{0@}@@pm.me".format(keyterm)) + elif keyterm.count("@@") == 2 and keyterm.startswith("@@") is False: + uidlist = keyterm.split("@@") + for uid in uidlist: + ksearch.append("@{0@}@@protonmail.com".format(uid)) + ksearch.append("@{0@}@@protonmail.ch".format(uid)) + ksearch.append("@{0@}@@pm.me".format(uid)) + elif keyterm.count("@@") > 2: + uidlist = keyterm.split("@@") + for uid in uidlist: + ksearch.append("@{0@}@@protonmail.com".format(uid)) + ksearch.append("@{0@}@@protonmail.ch".format(uid)) + ksearch.append("@{0@}@@pm.me".format(uid)) + else: + ksearch.append(keyterm) + +for k in ksearch: + print("Checking for key for: @{0@}".format(k)) + try: + keys = server.search(k) + if isinstance(keys, list) is True: + for key in keys: + allkeys.append(key) + try: + import_result = c.key_import(key.key_blob) + except Exception as e: + import_result = c.key_import(key.key) + else: + paradox.append(keys) + import_result = None + except Exception as e: + import_result = None + results.append(import_result) + +for result in results: + if result is not None and hasattr(result, "considered") is False: + print("@{0@} for @{1@}".format(result.decode(), k)) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" +The total number of keys considered for import was: @{0@} + +With UIDs wholely or partially matching the following string: + + @{1@} + + Number of keys revoked: @{2@} + Number of new signatures: @{3@} + Number of new subkeys: @{4@} + Number of new user IDs: @{5@} +Number of new secret keys: @{6@} + Number of unchanged keys: @{7@} + +The key IDs for all considered keys were: +""".format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + elif result is None: + pass +@end example + +@item +Import from ProtonMail via HKP for Python Example no. 2 + + +Like its counterpart above, this script can also be found with the +rest of the examples, by the name pmkey-import-hkp-alt.py. + +With this script a modicum of effort has been made to treat anything +passed as a @samp{homedir} which either does not exist or which is not a +directory, as also being a pssible user ID to check for. It's not +guaranteed to pick up on all such cases, but it should cover most of +them. + +@example +import gpg +import hkp4py +import os.path +import sys + +print(""" +This script searches the ProtonMail key server for the specified key and +imports it. Optionally enables specifying a different GnuPG home directory. + +Usage: pmkey-import-hkp.py [homedir] [search string] + or: pmkey-import-hkp.py [search string] +""") + +c = gpg.Context(armor=True) +server = hkp4py.KeyServer("hkps://api.protonmail.ch") +keyterms = [] +ksearch = [] +allkeys = [] +results = [] +paradox = [] +homeless = None + +if len(sys.argv) > 3: + homedir = sys.argv[1] + keyterms = sys.argv[2:] +elif len(sys.argv) == 3: + homedir = sys.argv[1] + keyterm = sys.argv[2] + keyterms.append(keyterm) +elif len(sys.argv) == 2: + homedir = "" + keyterm = sys.argv[1] + keyterms.append(keyterm) +else: + keyterm = input("Enter the key ID, UID or search string: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + keyterms.append(keyterm) + +if len(homedir) == 0: + homedir = None + homeless = False + +if homedir is not None: + if homedir.startswith("~"): + if os.path.exists(os.path.expanduser(homedir)) is True: + if os.path.isdir(os.path.expanduser(homedir)) is True: + c.home_dir = os.path.realpath(os.path.expanduser(homedir)) + else: + homeless = True + else: + homeless = True + elif os.path.exists(os.path.realpath(homedir)) is True: + if os.path.isdir(os.path.realpath(homedir)) is True: + c.home_dir = os.path.realpath(homedir) + else: + homeless = True + else: + homeless = True + +# First check to see if the homedir really is a homedir and if not, treat it as +# a search string. +if homeless is True: + keyterms.append(homedir) + c.home_dir = None +else: + pass + +for keyterm in keyterms: + if keyterm.count("@@") == 2 and keyterm.startswith("@@") is True: + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + elif keyterm.count("@@") == 1 and keyterm.startswith("@@") is True: + ksearch.append("@{0@}@@protonmail.com".format(keyterm[1:])) + ksearch.append("@{0@}@@protonmail.ch".format(keyterm[1:])) + ksearch.append("@{0@}@@pm.me".format(keyterm[1:])) + elif keyterm.count("@@") == 0: + ksearch.append("@{0@}@@protonmail.com".format(keyterm)) + ksearch.append("@{0@}@@protonmail.ch".format(keyterm)) + ksearch.append("@{0@}@@pm.me".format(keyterm)) + elif keyterm.count("@@") == 2 and keyterm.startswith("@@") is False: + uidlist = keyterm.split("@@") + for uid in uidlist: + ksearch.append("@{0@}@@protonmail.com".format(uid)) + ksearch.append("@{0@}@@protonmail.ch".format(uid)) + ksearch.append("@{0@}@@pm.me".format(uid)) + elif keyterm.count("@@") > 2: + uidlist = keyterm.split("@@") + for uid in uidlist: + ksearch.append("@{0@}@@protonmail.com".format(uid)) + ksearch.append("@{0@}@@protonmail.ch".format(uid)) + ksearch.append("@{0@}@@pm.me".format(uid)) + else: + ksearch.append(keyterm) + +for k in ksearch: + print("Checking for key for: @{0@}".format(k)) + try: + keys = server.search(k) + if isinstance(keys, list) is True: + for key in keys: + allkeys.append(key) + try: + import_result = c.key_import(key.key_blob) + except Exception as e: + import_result = c.key_import(key.key) + else: + paradox.append(keys) + import_result = None + except Exception as e: + import_result = None + results.append(import_result) + +for result in results: + if result is not None and hasattr(result, "considered") is False: + print("@{0@} for @{1@}".format(result.decode(), k)) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" +The total number of keys considered for import was: @{0@} + +With UIDs wholely or partially matching the following string: + + @{1@} + + Number of keys revoked: @{2@} + Number of new signatures: @{3@} + Number of new subkeys: @{4@} + Number of new user IDs: @{5@} +Number of new secret keys: @{6@} + Number of unchanged keys: @{7@} + +The key IDs for all considered keys were: +""".format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + elif result is None: + pass +@end example +@end enumerate + +@node Exporting keys +@section Exporting keys + +Exporting keys remains a reasonably simple task, but has been +separated into three different functions for the OpenPGP cryptographic +engine. Two of those functions are for exporting public keys and the +third is for exporting secret keys. + +@menu +* Exporting public keys:: +* Exporting secret keys:: +* Sending public keys to the SKS Keyservers:: +@end menu + +@node Exporting public keys +@subsection Exporting public keys + +There are two methods of exporting public keys, both of which are very +similar to the other. The default method, @samp{key_export()}, will export +a public key or keys matching a specified pattern as normal. The +alternative, the @samp{key_export_minimal()} method, will do the same thing +except producing a minimised output with extra signatures and third +party signatures or certifications removed. + +@example +import gpg +import os.path +import sys + +print(""" +This script exports one or more public keys. +""") + +c = gpg.Context(armor=True) + +if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] +elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") +elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + keyfile = input("Enter the path and filename to save the secret key to: ") + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if homedir.startswith("~"): + if os.path.exists(os.path.expanduser(homedir)) is True: + c.home_dir = os.path.expanduser(homedir) + else: + pass +elif os.path.exists(homedir) is True: + c.home_dir = homedir +else: + pass + +try: + result = c.key_export(pattern=logrus) +except: + result = c.key_export(pattern=None) + +if result is not None: + with open(keyfile, "wb") as f: + f.write(result) +else: + pass +@end example + +It should be noted that the result will only return @samp{None} when a +search pattern has been entered, but has not matched any keys. When +the search pattern itself is set to @samp{None} this triggers the exporting +of the entire public keybox. + +@example +import gpg +import os.path +import sys + +print(""" +This script exports one or more public keys in minimised form. +""") + +c = gpg.Context(armor=True) + +if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] +elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") +elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + keyfile = input("Enter the path and filename to save the secret key to: ") + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if homedir.startswith("~"): + if os.path.exists(os.path.expanduser(homedir)) is True: + c.home_dir = os.path.expanduser(homedir) + else: + pass +elif os.path.exists(homedir) is True: + c.home_dir = homedir +else: + pass + +try: + result = c.key_export_minimal(pattern=logrus) +except: + result = c.key_export_minimal(pattern=None) + +if result is not None: + with open(keyfile, "wb") as f: + f.write(result) +else: + pass +@end example + +@node Exporting secret keys +@subsection Exporting secret keys + +Exporting secret keys is, functionally, very similar to exporting +public keys; save for the invocation of @samp{pinentry} via @samp{gpg-agent} in +order to securely enter the key's passphrase and authorise the export. + +The following example exports the secret key to a file which is then +set with the same permissions as the output files created by the +command line secret key export options. + +@example +import gpg +import os +import os.path +import sys + +print(""" +This script exports one or more secret keys. + +The gpg-agent and pinentry are invoked to authorise the export. +""") + +c = gpg.Context(armor=True) + +if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] +elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") +elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + keyfile = input("Enter the path and filename to save the secret key to: ") + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if len(homedir) == 0: + homedir = None +elif homedir.startswith("~"): + userdir = os.path.expanduser(homedir) + if os.path.exists(userdir) is True: + homedir = os.path.realpath(userdir) + else: + homedir = None +else: + homedir = os.path.realpath(homedir) + +if os.path.exists(homedir) is False: + homedir = None +else: + if os.path.isdir(homedir) is False: + homedir = None + else: + pass + +if homedir is not None: + c.home_dir = homedir +else: + pass + +try: + result = c.key_export_secret(pattern=logrus) +except: + result = c.key_export_secret(pattern=None) + +if result is not None: + with open(keyfile, "wb") as f: + f.write(result) + os.chmod(keyfile, 0o600) +else: + pass +@end example + +Alternatively the approach of the following script can be used. This +longer example saves the exported secret key(s) in files in the GnuPG +home directory, in addition to setting the file permissions as only +readable and writable by the user. It also exports the secret key(s) +twice in order to output both GPG binary (@samp{.gpg}) and ASCII armoured +(@samp{.asc}) files. + +@example +import gpg +import os +import os.path +import subprocess +import sys + +print(""" +This script exports one or more secret keys as both ASCII armored and binary +file formats, saved in files within the user's GPG home directory. + +The gpg-agent and pinentry are invoked to authorise the export. +""") + +if sys.platform == "win32": + gpgconfcmd = "gpgconf.exe --list-dirs homedir" +else: + gpgconfcmd = "gpgconf --list-dirs homedir" + +a = gpg.Context(armor=True) +b = gpg.Context() +c = gpg.Context() + +if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] +elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") +elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + keyfile = input("Enter the filename to save the secret key to: ") + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if len(homedir) == 0: + homedir = None +elif homedir.startswith("~"): + userdir = os.path.expanduser(homedir) + if os.path.exists(userdir) is True: + homedir = os.path.realpath(userdir) + else: + homedir = None +else: + homedir = os.path.realpath(homedir) + +if os.path.exists(homedir) is False: + homedir = None +else: + if os.path.isdir(homedir) is False: + homedir = None + else: + pass + +if homedir is not None: + c.home_dir = homedir +else: + pass + +if c.home_dir is not None: + if c.home_dir.endswith("/"): + gpgfile = "@{0@}@{1@}.gpg".format(c.home_dir, keyfile) + ascfile = "@{0@}@{1@}.asc".format(c.home_dir, keyfile) + else: + gpgfile = "@{0@}/@{1@}.gpg".format(c.home_dir, keyfile) + ascfile = "@{0@}/@{1@}.asc".format(c.home_dir, keyfile) +else: + if os.path.exists(os.environ["GNUPGHOME"]) is True: + hd = os.environ["GNUPGHOME"] + else: + try: + hd = subprocess.getoutput(gpgconfcmd) + except: + process = subprocess.Popen(gpgconfcmd.split(), + stdout=subprocess.PIPE) + procom = process.communicate() + if sys.version_info[0] == 2: + hd = procom[0].strip() + else: + hd = procom[0].decode().strip() + gpgfile = "@{0@}/@{1@}.gpg".format(hd, keyfile) + ascfile = "@{0@}/@{1@}.asc".format(hd, keyfile) + +try: + a_result = a.key_export_secret(pattern=logrus) + b_result = b.key_export_secret(pattern=logrus) +except: + a_result = a.key_export_secret(pattern=None) + b_result = b.key_export_secret(pattern=None) + +if a_result is not None: + with open(ascfile, "wb") as f: + f.write(a_result) + os.chmod(ascfile, 0o600) +else: + pass + +if b_result is not None: + with open(gpgfile, "wb") as f: + f.write(b_result) + os.chmod(gpgfile, 0o600) +else: + pass +@end example + +@node Sending public keys to the SKS Keyservers +@subsection Sending public keys to the SKS Keyservers + +As with the previous section on importing keys, the @samp{hkp4py} module +adds another option with exporting keys in order to send them to the +public keyservers. + +The following example demonstrates how this may be done. + +@example +import gpg +import hkp4py +import os.path +import sys + +print(""" +This script sends one or more public keys to the SKS keyservers and is +essentially a slight variation on the export-key.py script. +""") + +c = gpg.Context(armor=True) +server = hkp4py.KeyServer("hkps://hkps.pool.sks-keyservers.net") + +if len(sys.argv) > 2: + logrus = " ".join(sys.argv[1:]) +elif len(sys.argv) == 2: + logrus = sys.argv[1] +else: + logrus = input("Enter the UID matching the key(s) to send: ") + +if len(logrus) > 0: + try: + export_result = c.key_export(pattern=logrus) + except Exception as e: + print(e) + export_result = None +else: + export_result = c.key_export(pattern=None) + +if export_result is not None: + try: + try: + send_result = server.add(export_result) + except: + send_result = server.add(export_result.decode()) + if send_result is not None: + print(send_result) + else: + pass + except Exception as e: + print(e) +else: + pass +@end example + +An expanded version of this script with additional functions for +specifying an alternative homedir location is in the examples +directory as @samp{send-key-to-keyserver.py}. + +The @samp{hkp4py} module appears to handle both string and byte literal text +data equally well, but the GPGME bindings deal primarily with byte +literal data only and so this script sends in that format first, then +tries the string literal form. + +@node Basic Functions +@chapter Basic Functions + +The most frequently called features of any cryptographic library will +be the most fundamental tasks for encryption software. In this +section we will look at how to programmatically encrypt data, decrypt +it, sign it and verify signatures. + +@menu +* Encryption:: +* Decryption:: +* Signing text and files:: +* Signature verification:: +@end menu + +@node Encryption +@section Encryption + +Encrypting is very straight forward. In the first example below the +message, @samp{text}, is encrypted to a single recipient's key. In the +second example the message will be encrypted to multiple recipients. + +@menu +* Encrypting to one key:: +* Encrypting to multiple keys:: +@end menu + +@node Encrypting to one key +@subsection Encrypting to one key + +Once the the Context is set the main issues with encrypting data is +essentially reduced to key selection and the keyword arguments +specified in the @samp{gpg.Context().encrypt()} method. + +Those keyword arguments are: @samp{recipients}, a list of keys encrypted to +(covered in greater detail in the following section); @samp{sign}, whether +or not to sign the plaintext data, see subsequent sections on signing +and verifying signatures below (defaults to @samp{True}); @samp{sink}, to write +results or partial results to a secure sink instead of returning it +(defaults to @samp{None}); @samp{passphrase}, only used when utilising symmetric +encryption (defaults to @samp{None}); @samp{always_trust}, used to override the +trust model settings for recipient keys (defaults to @samp{False}); +@samp{add_encrypt_to}, utilises any preconfigured @samp{encrypt-to} or +@samp{default-key} settings in the user's @samp{gpg.conf} file (defaults to +@samp{False}); @samp{prepare}, prepare for encryption (defaults to @samp{False}); +@samp{expect_sign}, prepare for signing (defaults to @samp{False}); @samp{compress}, +compresses the plaintext prior to encryption (defaults to @samp{True}). + +@example +import gpg + +a_key = "0x12345678DEADBEEF" +text = b"""Some text to test with. + +Since the text in this case must be bytes, it is most likely that +the input form will be a separate file which is opened with "rb" +as this is the simplest method of obtaining the correct data format. +""" + +c = gpg.Context(armor=True) +rkey = list(c.keylist(pattern=a_key, secret=False)) +ciphertext, result, sign_result = c.encrypt(text, recipients=rkey, sign=False) + +with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) +@end example + +Though this is even more likely to be used like this; with the +plaintext input read from a file, the recipient keys used for +encryption regardless of key trust status and the encrypted output +also encrypted to any preconfigured keys set in the @samp{gpg.conf} file: + +@example +import gpg + +a_key = "0x12345678DEADBEEF" + +with open("secret_plans.txt", "rb") as afile: + text = afile.read() + +c = gpg.Context(armor=True) +rkey = list(c.keylist(pattern=a_key, secret=False)) +ciphertext, result, sign_result = c.encrypt(text, recipients=rkey, sign=True, + always_trust=True, + add_encrypt_to=True) + +with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) +@end example + +If the @samp{recipients} paramater is empty then the plaintext is encrypted +symmetrically. If no @samp{passphrase} is supplied as a parameter or via a +callback registered with the @samp{Context()} then an out-of-band prompt +for the passphrase via pinentry will be invoked. + +@node Encrypting to multiple keys +@subsection Encrypting to multiple keys + +Encrypting to multiple keys essentially just expands upon the key +selection process and the recipients from the previous examples. + +The following example encrypts a message (@samp{text}) to everyone with an +email address on the @samp{gnupg.org} domain,@footnote{You probably don't really want to do this. Searching the +keyservers for "gnupg.org" produces over 400 results, the majority of +which aren't actually at the gnupg.org domain, but just included a +comment regarding the project in their key somewhere.} but does @emph{not} encrypt +to a default key or other key which is configured to normally encrypt +to. + +@example +import gpg + +text = b"""Oh look, another test message. + +The same rules apply as with the previous example and more likely +than not, the message will actually be drawn from reading the +contents of a file or, maybe, from entering data at an input() +prompt. + +Since the text in this case must be bytes, it is most likely that +the input form will be a separate file which is opened with "rb" +as this is the simplest method of obtaining the correct data +format. +""" + +c = gpg.Context(armor=True) +rpattern = list(c.keylist(pattern="@@gnupg.org", secret=False)) +logrus = [] + +for i in range(len(rpattern)): + if rpattern[i].can_encrypt == 1: + logrus.append(rpattern[i]) + +ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, + sign=False, always_trust=True) + +with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) +@end example + +All it would take to change the above example to sign the message +and also encrypt the message to any configured default keys would +be to change the @samp{c.encrypt} line to this: + +@example +ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, + always_trust=True, + add_encrypt_to=True) +@end example + +The only keyword arguments requiring modification are those for which +the default values are changing. The default value of @samp{sign} is +@samp{True}, the default of @samp{always_trust} is @samp{False}, the default of +@samp{add_encrypt_to} is @samp{False}. + +If @samp{always_trust} is not set to @samp{True} and any of the recipient keys +are not trusted (e.g. not signed or locally signed) then the +encryption will raise an error. It is possible to mitigate this +somewhat with something more like this: + +@example +import gpg + +with open("secret_plans.txt.asc", "rb") as afile: + text = afile.read() + +c = gpg.Context(armor=True) +rpattern = list(c.keylist(pattern="@@gnupg.org", secret=False)) +logrus = [] + +for i in range(len(rpattern)): + if rpattern[i].can_encrypt == 1: + logrus.append(rpattern[i]) + + try: + ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, + add_encrypt_to=True) + except gpg.errors.InvalidRecipients as e: + for i in range(len(e.recipients)): + for n in range(len(logrus)): + if logrus[n].fpr == e.recipients[i].fpr: + logrus.remove(logrus[n]) + else: + pass + try: + ciphertext, result, sign_result = c.encrypt(text, + recipients=logrus, + add_encrypt_to=True) + with open("secret_plans.txt.asc", "wb") as afile: + afile.write(ciphertext) + except: + pass +@end example + +This will attempt to encrypt to all the keys searched for, then remove +invalid recipients if it fails and try again. + +@node Decryption +@section Decryption + +Decrypting something encrypted to a key in one's secret keyring is +fairly straight forward. + +In this example code, however, preconfiguring either @samp{gpg.Context()} +or @samp{gpg.core.Context()} as @samp{c} is unnecessary because there is no need +to modify the Context prior to conducting the decryption and since the +Context is only used once, setting it to @samp{c} simply adds lines for no +gain. + +@example +import gpg + +ciphertext = input("Enter path and filename of encrypted file: ") +newfile = input("Enter path and filename of file to save decrypted data to: ") + +with open(ciphertext, "rb") as cfile: + try: + plaintext, result, verify_result = gpg.Context().decrypt(cfile) + except gpg.errors.GPGMEError as e: + plaintext = None + print(e) + +if plaintext is not None: + with open(newfile, "wb") as nfile: + nfile.write(plaintext) + else: + pass +@end example + +The data available in @samp{plaintext} in this example is the decrypted +content as a byte object, the recipient key IDs and algorithms in +@samp{result} and the results of verifying any signatures of the data in +@samp{verify_result}. + +@node Signing text and files +@section Signing text and files + +The following sections demonstrate how to specify keys to sign with. + +@menu +* Signing key selection:: +* Normal or default signing messages or files:: +* Detached signing messages and files:: +* Clearsigning messages or text:: +@end menu + +@node Signing key selection +@subsection Signing key selection + +By default GPGME and the Python bindings will use the default key +configured for the user invoking the GPGME API. If there is no +default key specified and there is more than one secret key available +it may be necessary to specify the key or keys with which to sign +messages and files. + +@example +import gpg + +logrus = input("Enter the email address or string to match signing keys to: ") +hancock = gpg.Context().keylist(pattern=logrus, secret=True) +sig_src = list(hancock) +@end example + +The signing examples in the following sections include the explicitly +designated @samp{signers} parameter in two of the five examples; once where +the resulting signature would be ASCII armoured and once where it +would not be armoured. + +While it would be possible to enter a key ID or fingerprint here to +match a specific key, it is not possible to enter two fingerprints and +match two keys since the patten expects a string, bytes or None and +not a list. A string with two fingerprints won't match any single +key. + +@node Normal or default signing messages or files +@subsection Normal or default signing messages or files + +The normal or default signing process is essentially the same as is +most often invoked when also encrypting a message or file. So when +the encryption component is not utilised, the result is to produce an +encoded and signed output which may or may not be ASCII armoured and +which may or may not also be compressed. + +By default compression will be used unless GnuPG detects that the +plaintext is already compressed. ASCII armouring will be determined +according to the value of @samp{gpg.Context().armor}. + +The compression algorithm is selected in much the same way as the +symmetric encryption algorithm or the hash digest algorithm is when +multiple keys are involved; from the preferences saved into the key +itself or by comparison with the preferences with all other keys +involved. + +@example +import gpg + +text0 = """Declaration of ... something. + +""" +text = text0.encode() + +c = gpg.Context(armor=True, signers=sig_src) +signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.NORMAL) + +with open("/path/to/statement.txt.asc", "w") as afile: + afile.write(signed_data.decode()) +@end example + +Though everything in this example is accurate, it is more likely that +reading the input data from another file and writing the result to a +new file will be performed more like the way it is done in the next +example. Even if the output format is ASCII armoured. + +@example +import gpg + +with open("/path/to/statement.txt", "rb") as tfile: + text = tfile.read() + +c = gpg.Context() +signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.NORMAL) + +with open("/path/to/statement.txt.sig", "wb") as afile: + afile.write(signed_data) +@end example + +@node Detached signing messages and files +@subsection Detached signing messages and files + +Detached signatures will often be needed in programmatic uses of +GPGME, either for signing files (e.g. tarballs of code releases) or as +a component of message signing (e.g. PGP/MIME encoded email). + +@example +import gpg + +text0 = """Declaration of ... something. + +""" +text = text0.encode() + +c = gpg.Context(armor=True) +signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.DETACH) + +with open("/path/to/statement.txt.asc", "w") as afile: + afile.write(signed_data.decode()) +@end example + +As with normal signatures, detached signatures are best handled as +byte literals, even when the output is ASCII armoured. + +@example +import gpg + +with open("/path/to/statement.txt", "rb") as tfile: + text = tfile.read() + +c = gpg.Context(signers=sig_src) +signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.DETACH) + +with open("/path/to/statement.txt.sig", "wb") as afile: + afile.write(signed_data) +@end example + +@node Clearsigning messages or text +@subsection Clearsigning messages or text + +Though PGP/in-line messages are no longer encouraged in favour of +PGP/MIME, there is still sometimes value in utilising in-line +signatures. This is where clear-signed messages or text is of value. + +@example +import gpg + +text0 = """Declaration of ... something. + +""" +text = text0.encode() + +c = gpg.Context() +signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.CLEAR) + +with open("/path/to/statement.txt.asc", "w") as afile: + afile.write(signed_data.decode()) +@end example + +In spite of the appearance of a clear-signed message, the data handled +by GPGME in signing it must still be byte literals. + +@example +import gpg + +with open("/path/to/statement.txt", "rb") as tfile: + text = tfile.read() + +c = gpg.Context() +signed_data, result = c.sign(text, mode=gpg.constants.sig.mode.CLEAR) + +with open("/path/to/statement.txt.asc", "wb") as afile: + afile.write(signed_data) +@end example + +@node Signature verification +@section Signature verification + +Essentially there are two principal methods of verification of a +signature. The first of these is for use with the normal or default +signing method and for clear-signed messages. The second is for use +with files and data with detached signatures. + +The following example is intended for use with the default signing +method where the file was not ASCII armoured: + +@example +import gpg +import time + +filename = "statement.txt" +gpg_file = "statement.txt.gpg" + +c = gpg.Context() + +try: + data, result = c.verify(open(gpg_file)) + verified = True +except gpg.errors.BadSignatures as e: + verified = False + print(e) + +if verified is True: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: +@{0@} +with key @{1@} +made at @{2@} +""".format(c.get_key(sign.fpr).uids[0].uid, sign.fpr, + time.ctime(sign.timestamp))) +else: + pass +@end example + +Whereas this next example, which is almost identical would work with +normal ASCII armoured files and with clear-signed files: + +@example +import gpg +import time + +filename = "statement.txt" +asc_file = "statement.txt.asc" + +c = gpg.Context() + +try: + data, result = c.verify(open(asc_file)) + verified = True +except gpg.errors.BadSignatures as e: + verified = False + print(e) + +if verified is True: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: +@{0@} +with key @{1@} +made at @{2@} +""".format(c.get_key(sign.fpr).uids[0].uid, sign.fpr, + time.ctime(sign.timestamp))) +else: + pass +@end example + +In both of the previous examples it is also possible to compare the +original data that was signed against the signed data in @samp{data} to see +if it matches with something like this: + +@example +with open(filename, "rb") as afile: + text = afile.read() + +if text == data: + print("Good signature.") +else: + pass +@end example + +The following two examples, however, deal with detached signatures. +With his method of verification the data that was signed does not get +returned since it is already being explicitly referenced in the first +argument of @samp{c.verify}. So @samp{data} is @samp{None} and only the information +in @samp{result} is available. + +@example +import gpg +import time + +filename = "statement.txt" +sig_file = "statement.txt.sig" + +c = gpg.Context() + +try: + data, result = c.verify(open(filename), open(sig_file)) + verified = True +except gpg.errors.BadSignatures as e: + verified = False + print(e) + +if verified is True: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: +@{0@} +with key @{1@} +made at @{2@} +""".format(c.get_key(sign.fpr).uids[0].uid, sign.fpr, + time.ctime(sign.timestamp))) +else: + pass +@end example + +@example +import gpg +import time + +filename = "statement.txt" +asc_file = "statement.txt.asc" + +c = gpg.Context() + +try: + data, result = c.verify(open(filename), open(asc_file)) + verified = True +except gpg.errors.BadSignatures as e: + verified = False + print(e) + +if verified is True: + for i in range(len(result.signatures)): + sign = result.signatures[i] + print("""Good signature from: +@{0@} +with key @{1@} +made at @{2@} +""".format(c.get_key(sign.fpr).uids[0].uid, sign.fpr, + time.ctime(sign.timestamp))) +else: + pass +@end example + +@node Creating keys and subkeys +@chapter Creating keys and subkeys + +The one thing, aside from GnuPG itself, that GPGME depends on, of +course, is the keys themselves. So it is necessary to be able to +generate them and modify them by adding subkeys, revoking or disabling +them, sometimes deleting them and doing the same for user IDs. + +In the following examples a key will be created for the world's +greatest secret agent, Danger Mouse. Since Danger Mouse is a secret +agent he needs to be able to protect information to @samp{SECRET} level +clearance, so his keys will be 3072-bit keys. + +The pre-configured @samp{gpg.conf} file which sets cipher, digest and other +preferences contains the following configuration parameters: + +@example +expert +allow-freeform-uid +allow-secret-key-import +trust-model tofu+pgp +tofu-default-policy unknown +enable-large-rsa +enable-dsa2 +cert-digest-algo SHA512 +default-preference-list TWOFISH CAMELLIA256 AES256 CAMELLIA192 AES192 CAMELLIA128 AES BLOWFISH IDEA CAST5 3DES SHA512 SHA384 SHA256 SHA224 RIPEMD160 SHA1 ZLIB BZIP2 ZIP Uncompressed +personal-cipher-preferences TWOFISH CAMELLIA256 AES256 CAMELLIA192 AES192 CAMELLIA128 AES BLOWFISH IDEA CAST5 3DES +personal-digest-preferences SHA512 SHA384 SHA256 SHA224 RIPEMD160 SHA1 +personal-compress-preferences ZLIB BZIP2 ZIP Uncompressed +@end example + +@menu +* Primary key:: +* Subkeys:: +* User IDs:: +* Key certification:: +@end menu + +@node Primary key +@section Primary key + +Generating a primary key uses the @samp{create_key} method in a Context. +It contains multiple arguments and keyword arguments, including: +@samp{userid}, @samp{algorithm}, @samp{expires_in}, @samp{expires}, @samp{sign}, @samp{encrypt}, +@samp{certify}, @samp{authenticate}, @samp{passphrase} and @samp{force}. The defaults for +all of those except @samp{userid}, @samp{algorithm}, @samp{expires_in}, @samp{expires} and +@samp{passphrase} is @samp{False}. The defaults for @samp{algorithm} and +@samp{passphrase} is @samp{None}. The default for @samp{expires_in} is @samp{0}. The +default for @samp{expires} is @samp{True}. There is no default for @samp{userid}. + +If @samp{passphrase} is left as @samp{None} then the key will not be generated +with a passphrase, if @samp{passphrase} is set to a string then that will +be the passphrase and if @samp{passphrase} is set to @samp{True} then gpg-agent +will launch pinentry to prompt for a passphrase. For the sake of +convenience, these examples will keep @samp{passphrase} set to @samp{None}. + +@example +import gpg + +c = gpg.Context() + +c.home_dir = "~/.gnupg-dm" +userid = "Danger Mouse <dm@@secret.example.net>" + +dmkey = c.create_key(userid, algorithm="rsa3072", expires_in=31536000, + sign=True, certify=True) +@end example + +One thing to note here is the use of setting the @samp{c.home_dir} +parameter. This enables generating the key or keys in a different +location. In this case to keep the new key data created for this +example in a separate location rather than adding it to existing and +active key store data. As with the default directory, @samp{~/.gnupg}, any +temporary or separate directory needs the permissions set to only +permit access by the directory owner. On posix systems this means +setting the directory permissions to 700. + +The @samp{temp-homedir-config.py} script in the HOWTO examples directory +will create an alternative homedir with these configuration options +already set and the correct directory and file permissions. + +The successful generation of the key can be confirmed via the returned +@samp{GenkeyResult} object, which includes the following data: + +@example +print(""" + Fingerprint: @{0@} + Primary Key: @{1@} + Public Key: @{2@} + Secret Key: @{3@} + Sub Key: @{4@} +User IDs: @{5@} +""".format(dmkey.fpr, dmkey.primary, dmkey.pubkey, dmkey.seckey, dmkey.sub, + dmkey.uid)) +@end example + +Alternatively the information can be confirmed using the command line +program: + +@example +bash-4.4$ gpg --homedir ~/.gnupg-dm -K +~/.gnupg-dm/pubring.kbx +---------------------- +sec rsa3072 2018-03-15 [SC] [expires: 2019-03-15] + 177B7C25DB99745EE2EE13ED026D2F19E99E63AA +uid [ultimate] Danger Mouse <dm@@secret.example.net> + +bash-4.4$ +@end example + +As with generating keys manually, to preconfigure expanded preferences +for the cipher, digest and compression algorithms, the @samp{gpg.conf} file +must contain those details in the home directory in which the new key +is being generated. I used a cut down version of my own @samp{gpg.conf} +file in order to be able to generate this: + +@example +bash-4.4$ gpg --homedir ~/.gnupg-dm --edit-key 177B7C25DB99745EE2EE13ED026D2F19E99E63AA showpref quit +Secret key is available. + +sec rsa3072/026D2F19E99E63AA + created: 2018-03-15 expires: 2019-03-15 usage: SC + trust: ultimate validity: ultimate +[ultimate] (1). Danger Mouse <dm@@secret.example.net> + +[ultimate] (1). Danger Mouse <dm@@secret.example.net> + Cipher: TWOFISH, CAMELLIA256, AES256, CAMELLIA192, AES192, CAMELLIA128, AES, BLOWFISH, IDEA, CAST5, 3DES + Digest: SHA512, SHA384, SHA256, SHA224, RIPEMD160, SHA1 + Compression: ZLIB, BZIP2, ZIP, Uncompressed + Features: MDC, Keyserver no-modify + +bash-4.4$ +@end example + +@node Subkeys +@section Subkeys + +Adding subkeys to a primary key is fairly similar to creating the +primary key with the @samp{create_subkey} method. Most of the arguments +are the same, but not quite all. Instead of the @samp{userid} argument +there is now a @samp{key} argument for selecting which primary key to add +the subkey to. + +In the following example an encryption subkey will be added to the +primary key. Since Danger Mouse is a security conscious secret agent, +this subkey will only be valid for about six months, half the length +of the primary key. + +@example +import gpg + +c = gpg.Context() +c.home_dir = "~/.gnupg-dm" + +key = c.get_key(dmkey.fpr, secret=True) +dmsub = c.create_subkey(key, algorithm="rsa3072", expires_in=15768000, + encrypt=True) +@end example + +As with the primary key, the results here can be checked with: + +@example +print(""" + Fingerprint: @{0@} + Primary Key: @{1@} + Public Key: @{2@} + Secret Key: @{3@} + Sub Key: @{4@} +User IDs: @{5@} +""".format(dmsub.fpr, dmsub.primary, dmsub.pubkey, dmsub.seckey, dmsub.sub, + dmsub.uid)) +@end example + +As well as on the command line with: + +@example +bash-4.4$ gpg --homedir ~/.gnupg-dm -K +~/.gnupg-dm/pubring.kbx +---------------------- +sec rsa3072 2018-03-15 [SC] [expires: 2019-03-15] + 177B7C25DB99745EE2EE13ED026D2F19E99E63AA +uid [ultimate] Danger Mouse <dm@@secret.example.net> +ssb rsa3072 2018-03-15 [E] [expires: 2018-09-13] + +bash-4.4$ +@end example + +@node User IDs +@section User IDs + +@menu +* Adding User IDs:: +* Revokinging User IDs:: +@end menu + +@node Adding User IDs +@subsection Adding User IDs + +By comparison to creating primary keys and subkeys, adding a new user +ID to an existing key is much simpler. The method used to do this is +@samp{key_add_uid} and the only arguments it takes are for the @samp{key} and +the new @samp{uid}. + +@example +import gpg + +c = gpg.Context() +c.home_dir = "~/.gnupg-dm" + +dmfpr = "177B7C25DB99745EE2EE13ED026D2F19E99E63AA" +key = c.get_key(dmfpr, secret=True) +uid = "Danger Mouse <danger.mouse@@secret.example.net>" + +c.key_add_uid(key, uid) +@end example + +Unsurprisingly the result of this is: + +@example +bash-4.4$ gpg --homedir ~/.gnupg-dm -K +~/.gnupg-dm/pubring.kbx +---------------------- +sec rsa3072 2018-03-15 [SC] [expires: 2019-03-15] + 177B7C25DB99745EE2EE13ED026D2F19E99E63AA +uid [ultimate] Danger Mouse <danger.mouse@@secret.example.net> +uid [ultimate] Danger Mouse <dm@@secret.example.net> +ssb rsa3072 2018-03-15 [E] [expires: 2018-09-13] + +bash-4.4$ +@end example + +@node Revokinging User IDs +@subsection Revokinging User IDs + +Revoking a user ID is a fairly similar process, except that it uses +the @samp{key_revoke_uid} method. + +@example +import gpg + +c = gpg.Context() +c.home_dir = "~/.gnupg-dm" + +dmfpr = "177B7C25DB99745EE2EE13ED026D2F19E99E63AA" +key = c.get_key(dmfpr, secret=True) +uid = "Danger Mouse <danger.mouse@@secret.example.net>" + +c.key_revoke_uid(key, uid) +@end example + +@node Key certification +@section Key certification + +Since key certification is more frequently referred to as key signing, +the method used to perform this function is @samp{key_sign}. + +The @samp{key_sign} method takes four arguments: @samp{key}, @samp{uids}, +@samp{expires_in} and @samp{local}. The default value of @samp{uids} is @samp{None} and +which results in all user IDs being selected. The default value of +both @samp{expires_in} and @samp{local} is @samp{False}; which results in the +signature never expiring and being able to be exported. + +The @samp{key} is the key being signed rather than the key doing the +signing. To change the key doing the signing refer to the signing key +selection above for signing messages and files. + +If the @samp{uids} value is not @samp{None} then it must either be a string to +match a single user ID or a list of strings to match multiple user +IDs. In this case the matching of those strings must be precise and +it is case sensitive. + +To sign Danger Mouse's key for just the initial user ID with a +signature which will last a little over a month, do this: + +@example +import gpg + +c = gpg.Context() +uid = "Danger Mouse <dm@@secret.example.net>" + +dmfpr = "177B7C25DB99745EE2EE13ED026D2F19E99E63AA" +key = c.get_key(dmfpr, secret=True) +c.key_sign(key, uids=uid, expires_in=2764800) +@end example + +@node Advanced or Experimental Use Cases +@chapter Advanced or Experimental Use Cases + +@menu +* C plus Python plus SWIG plus Cython:: +@end menu + +@node C plus Python plus SWIG plus Cython +@section C plus Python plus SWIG plus Cython + +In spite of the apparent incongruence of using Python bindings to a C +interface only to generate more C from the Python; it is in fact quite +possible to use the GPGME bindings with @uref{http://docs.cython.org/en/latest/index.html, Cython}. Though in many cases +the benefits may not be obvious since the most computationally +intensive work never leaves the level of the C code with which GPGME +itself is interacting with. + +Nevertheless, there are some situations where the benefits are +demonstrable. One of the better and easier examples being the one of +the early examples in this HOWTO, the @ref{Counting keys, , key counting} code. Running that +example as an executable Python script, @samp{keycount.py} (available in +the @samp{examples/howto/} directory), will take a noticable amount of time +to run on most systems where the public keybox or keyring contains a +few thousand public keys. + +Earlier in the evening, prior to starting this section, I ran that +script on my laptop; as I tend to do periodically and timed it using +@samp{time} utility, with the following results: + +@example +bash-4.4$ time keycount.py + +Number of secret keys: 23 +Number of public keys: 12112 + + +real 11m52.945s +user 0m0.913s +sys 0m0.752s + +bash-4.4$ +@end example + +Sometime after that I imported another key and followed it with a +little test of Cython. This test was kept fairly basic, essentially +lifting the material from the @uref{http://docs.cython.org/en/latest/src/tutorial/cython_tutorial.html, Cython Basic Tutorial} to demonstrate +compiling Python code to C. The first step was to take the example +key counting code quoted previously, essentially from the importing of +the @samp{gpg} module to the end of the script: + +@example +import gpg + +c = gpg.Context() +seckeys = c.keylist(pattern=None, secret=True) +pubkeys = c.keylist(pattern=None, secret=False) + +seclist = list(seckeys) +secnum = len(seclist) + +publist = list(pubkeys) +pubnum = len(publist) + +print(""" + Number of secret keys: @{0@} + Number of public keys: @{1@} + +""".format(secnum, pubnum)) +@end example + +Save that into a file called @samp{keycount.pyx} and then create a +@samp{setup.py} file which contains this: + +@example +from distutils.core import setup +from Cython.Build import cythonize + +setup( + ext_modules = cythonize("keycount.pyx") +) +@end example + +Compile it: + +@example +bash-4.4$ python setup.py build_ext --inplace +bash-4.4$ +@end example + +Then run it in a similar manner to @samp{keycount.py}: + +@example +bash-4.4$ time python3.7 -c "import keycount" + +Number of secret keys: 23 +Number of public keys: 12113 + + +real 6m47.905s +user 0m0.785s +sys 0m0.331s + +bash-4.4$ +@end example + +Cython turned @samp{keycount.pyx} into an 81KB @samp{keycount.o} file in the +@samp{build/} directory, a 24KB @samp{keycount.cpython-37m-darwin.so} file to be +imported into Python 3.7 and a 113KB @samp{keycount.c} generated C source +code file of nearly three thousand lines. Quite a bit bigger than the +314 bytes of the @samp{keycount.pyx} file or the full 1,452 bytes of the +full executable @samp{keycount.py} example script. + +On the other hand it ran in nearly half the time; taking 6 minutes and +47.905 seconds to run. As opposed to the 11 minutes and 52.945 seconds +which the CPython script alone took. + +The @samp{keycount.pyx} and @samp{setup.py} files used to generate this example +have been added to the @samp{examples/howto/advanced/cython/} directory +The example versions include some additional options to annotate the +existing code and to detect Cython's use. The latter comes from the +@uref{http://docs.cython.org/en/latest/src/tutorial/pure.html#magic-attributes-within-the-pxd, Magic Attributes} section of the Cython documentation. + +@node Miscellaneous extras and work-arounds +@chapter Miscellaneous extras and work-arounds + +Most of the things in the following sections are here simply because +there was no better place to put them, even though some are only +peripherally related to the GPGME Python bindings. Some are also +workarounds for functions not integrated with GPGME as yet. This is +especially true of the first of these, dealing with @ref{Group lines, , group lines}. + +@menu +* Group lines:: +* Keyserver access for Python:: +@end menu + +@node Group lines +@section Group lines + +There is not yet an easy way to access groups configured in the +gpg.conf file from within GPGME. As a consequence these central +groupings of keys cannot be shared amongst multiple programs, such as +MUAs readily. + +The following code, however, provides a work-around for obtaining this +information in Python. + +@example +import subprocess +import sys + +if sys.platform == "win32": + gpgconfcmd = "gpgconf.exe --list-options gpg" +else: + gpgconfcmd = "gpgconf --list-options gpg" + +try: + lines = subprocess.getoutput(gpgconfcmd).splitlines() +except: + process = subprocess.Popen(gpgconfcmd.split(), stdout=subprocess.PIPE) + procom = process.communicate() + if sys.version_info[0] == 2: + lines = procom[0].splitlines() + else: + lines = procom[0].decode().splitlines() + +for i in range(len(lines)): + if lines[i].startswith("group") is True: + line = lines[i] + else: + pass + +groups = line.split(":")[-1].replace('"', '').split(',') + +group_lines = [] +group_lists = [] + +for i in range(len(groups)): + group_lines.append(groups[i].split("=")) + group_lists.append(groups[i].split("=")) + +for i in range(len(group_lists)): + group_lists[i][1] = group_lists[i][1].split() +@end example + +The result of that code is that @samp{group_lines} is a list of lists where +@samp{group_lines[i][0]} is the name of the group and @samp{group_lines[i][1]} +is the key IDs of the group as a string. + +The @samp{group_lists} result is very similar in that it is a list of +lists. The first part, @samp{group_lists[i][0]} matches +@samp{group_lines[i][0]} as the name of the group, but @samp{group_lists[i][1]} +is the key IDs of the group as a string. + +A demonstration of using the @samp{groups.py} module is also available in +the form of the executable @samp{mutt-groups.py} script. This second +script reads all the group entries in a user's @samp{gpg.conf} file and +converts them into crypt-hooks suitable for use with the Mutt and +Neomutt mail clients. + +@node Keyserver access for Python +@section Keyserver access for Python + +The @uref{https://github.com/Selfnet/hkp4py, hkp4py} module by Marcel Fest was originally a port of the old +@uref{https://github.com/dgladkov/python-hkp, python-hkp} module from Python 2 to Python 3 and updated to use the +@uref{http://docs.python-requests.org/en/latest/index.html, requests} module instead. It has since been modified to provide +support for Python 2.7 as well and is available via PyPI. + +Since it rewrites the @samp{hkp} protocol prefix as @samp{http} and @samp{hkps} as +@samp{https}, the module is able to be used even with servers which do not +support the full scope of keyserver functions.@footnote{Such as with ProtonMail servers. This also means that +restricted servers which only advertise either HTTP or HTTPS end +points and not HKP or HKPS end points must still be identified as as +HKP or HKPS within the Python Code. The @samp{hkp4py} module will rewrite +these appropriately when the connection is made to the server.} It also works quite +readily when incorporated into a @ref{C plus Python plus SWIG plus Cython, , Cython} generated and compiled version +of any code. + +@menu +* Key import format:: +@end menu + +@node Key import format +@subsection Key import format + +The hkp4py module returns key data via requests as string literals +(@samp{r.text}) instead of byte literals (@samp{r.content}). This means that +the retrurned key data must be encoded to UTF-8 when importing that +key material using a @samp{gpg.Context().key_import()} method. + +For this reason an alternative method has been added to the @samp{search} +function of @samp{hkp4py.KeyServer()} which returns the key in the correct +format as expected by @samp{key_import}. When importing using this module, +it is now possible to import with this: + +@example +for key in keys: + if key.revoked is False: + gpg.Context().key_import(key.key_blob) + else: + pass +@end example + +Without that recent addition it would have been necessary to encode +the contents of each @samp{hkp4py.KeyServer().search()[i].key} in +@samp{hkp4py.KeyServer().search()} before trying to import it. + +An example of this is included in the @ref{Importing keys, , Importing Keys} section of this +HOWTO and the corresponding executable version of that example is +available in the @samp{lang/python/examples/howto} directory as normal; the +executable version is the @samp{import-keys-hkp.py} file. + +@node Copyright and Licensing +@chapter Copyright and Licensing + +@menu +* Copyright:: +* Draft Editions of this HOWTO:: +* License GPL compatible:: +@end menu + +@node Copyright +@section Copyright + +Copyright © The GnuPG Project, 2018. + +Copyright (C) The GnuPG Project, 2018. + +@node Draft Editions of this HOWTO +@section Draft Editions of this HOWTO + +Draft editions of this HOWTO may be periodically available directly +from the author at any of the following URLs: + +@itemize +@item +@uref{https://files.au.adversary.org/crypto/gpgme-python-howto.html, GPGME Python Bindings HOWTO draft (XHTML AWS S3 SSL)} +@item +@uref{http://files.au.adversary.org/crypto/gpgme-python-howto.html, GPGME Python Bindings HOWTO draft (XHTML AWS S3 no SSL)} +@item +@uref{https://files.au.adversary.org/crypto/gpgme-python-howto.texi, GPGME Python Bindings HOWTO draft (Texinfo file AWS S3 SSL)} +@item +@uref{http://files.au.adversary.org/crypto/gpgme-python-howto.texi, GPGME Python Bindings HOWTO draft (Texinfo file AWS S3 no SSL)} +@item +@uref{https://files.au.adversary.org/crypto/gpgme-python-howto.info, GPGME Python Bindings HOWTO draft (Info file AWS S3 SSL)} +@item +@uref{http://files.au.adversary.org/crypto/gpgme-python-howto.info, GPGME Python Bindings HOWTO draft (Info file AWS S3 no SSL)} +@item +@uref{https://files.au.adversary.org/crypto/gpgme-python-howto.rst, GPGME Python Bindings HOWTO draft (reST file AWS S3 SSL)} +@item +@uref{http://files.au.adversary.org/crypto/gpgme-python-howto.rst, GPGME Python Bindings HOWTO draft (reST file AWS S3 no SSL)} +@item +@uref{https://files.au.adversary.org/crypto/gpgme-python-howto.xml, GPGME Python Bindings HOWTO draft (Docbook 4.2 AWS S3 SSL)} +@item +@uref{http://files.au.adversary.org/crypto/gpgme-python-howto.xml, GPGME Python Bindings HOWTO draft (Docbook 4.2 AWS S3 no SSL)} +@end itemize + +All of these draft versions except for one have been generated from +this document via Emacs @uref{https://orgmode.org/, Org mode} and @uref{https://www.gnu.org/software/texinfo/, GNU Texinfo}. Though it is likely +that the specific @uref{https://files.au.adversary.org/crypto/gpgme-python-howto.org, file} @uref{http://files.au.adversary.org/crypto/gpgme-python-howto.org, version} used will be on the same server with +the generated output formats. + +The one exception is the reStructuredText version, which was converted +using the latest version of Pandoc from the Org mode source file using +the following command: + +@example +pandoc -f org -t rst+smart -o gpgme-python-howto.rst gpgme-python-howto.org +@end example + +In addition to these there is a significantly less frequently updated +version as a HTML @uref{https://files.au.adversary.org/crypto/gpgme-python-howto/webhelp/index.html, WebHelp site} (AWS S3 SSL); generated from DITA XML +source files, which can be found in @uref{https://dev.gnupg.org/source/gpgme/browse/ben%252Fhowto-dita/, an alternative branch} of the GPGME +git repository. + +These draft editions are not official documents and the version of +documentation in the master branch or which ships with released +versions is the only official documentation. Nevertheless, these +draft editions may occasionally be of use by providing more accessible +web versions which are updated between releases. They are provided on +the understanding that they may contain errors or may contain content +subject to change prior to an official release. + +@node License GPL compatible +@section License GPL compatible + +This file is free software; as a special exception the author gives +unlimited permission to copy and/or distribute it, with or without +modifications, as long as this notice is preserved. + +This file is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY, to the extent permitted by law; without even the +implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +PURPOSE. + +@bye
\ No newline at end of file diff --git a/lang/python/doc/texinfo/index.texi b/lang/python/doc/texinfo/index.texi new file mode 100644 index 0000000..4f80423 --- /dev/null +++ b/lang/python/doc/texinfo/index.texi @@ -0,0 +1,52 @@ +\input texinfo @c -*- texinfo -*- +@c %**start of header +@setfilename index.info +@settitle GNU Privacy Guard (GnuPG) Made Easy Python Bindings +@documentencoding UTF-8 +@documentlanguage en +@c %**end of header + +@finalout +@titlepage +@title GNU Privacy Guard (GnuPG) Made Easy Python Bindings +@author Ben McGinnes +@end titlepage + +@contents + +@ifnottex +@node Top +@top GNU Privacy Guard (GnuPG) Made Easy Python Bindings +@end ifnottex + +@menu +* GPGME Python Bindings:: + +@detailmenu +--- The Detailed Node Listing --- + +GPGME Python Bindings + +* Contents:: + +@end detailmenu +@end menu + +@node GPGME Python Bindings +@chapter GPGME Python Bindings + +@menu +* Contents:: +@end menu + +@node Contents +@section Contents + +@itemize +@item +@uref{short-history.org, A short history of the project} +@item +@uref{gpgme-python-howto.org, GPGME Python Bindings HOWTO} +@end itemize + +@bye
\ No newline at end of file diff --git a/lang/python/doc/texinfo/short-history.texi b/lang/python/doc/texinfo/short-history.texi new file mode 100644 index 0000000..a982f02 --- /dev/null +++ b/lang/python/doc/texinfo/short-history.texi @@ -0,0 +1,209 @@ +\input texinfo @c -*- texinfo -*- +@c %**start of header +@setfilename short-history.info +@settitle A Short History of the GPGME bindings for Python +@documentencoding UTF-8 +@documentlanguage en +@c %**end of header + +@finalout +@titlepage +@title A Short History of the GPGME bindings for Python +@author Ben McGinnes +@end titlepage + +@contents + +@ifnottex +@node Top +@top A Short History of the GPGME bindings for Python +@end ifnottex + +@menu +* Overview:: +* Relics of the past:: + +@detailmenu +--- The Detailed Node Listing --- + +Overview + +* In the beginning:: +* Keeping the flame alive:: +* Passing the torch:: +* Coming full circle:: + +Relics of the past + +* The Annoyances of Git:: +* The Perils of PyPI:: + +The Perils of PyPI + +* GPG 1·8·0 - Python bindings for GPGME GnuPG cryptography library:: +* PyME 0·9·0 - Python support for GPGME GnuPG cryptography library:: + +@end detailmenu +@end menu + +@node Overview +@chapter Overview + +The GPGME Python bindings passed through many hands and numerous +phases before, after a fifteen year journey, coming full circle to +return to the source. This is a short explanation of that journey. + +@menu +* In the beginning:: +* Keeping the flame alive:: +* Passing the torch:: +* Coming full circle:: +@end menu + +@node In the beginning +@section In the beginning + +In 2002 John Goerzen released PyME; Python bindings for the GPGME +module which utilised the current release of Python of the time and +SWIG.@footnote{In all likelihood thos would have been Python 2.2 or possibly +Python 2.3.} Shortly after creating it and ensuring it worked he stopped +supporting it, though he left his work available on his Gopher +site. + +@node Keeping the flame alive +@section Keeping the flame alive + +A couple of years later the project was picked up by Igor Belyi and +actively developed and maintained by him from 2004 to 2008. Igor's +whereabouts at the time of this document's creation are unknown, +but the current authors do hope he is well. We're assuming (or +hoping) that life did what life does and made continuing untenable. + +@node Passing the torch +@section Passing the torch + +In 2014 Martin Albrecht wanted to patch a bug in the PyME code and +discovered the absence of Igor. Following a discussion on the PyME +mailing list he became the new maintainer for PyME, releasing +version 0.9.0 in May of that year. He remains the maintainer of +the original PyME release in Python 2.6 and 2.7 (available via +PyPI). + +@node Coming full circle +@section Coming full circle + +In 2015 Ben McGinnes approached Martin about a Python 3 version, +while investigating how complex a task this would be the task ended +up being completed. A subsequent discussion with Werner Koch led +to the decision to fold the Python 3 port back into the original +GPGME release in the languages subdirectory for non-C bindings +under the module name of @samp{pyme3}. + +In 2016 this PyME module was integrated back into the GPGME project +by Justus Winter. During the course of this work Justus adjusted +the port to restore limited support for Python 2, but not as many +minor point releases as the original PyME package supports. During +the course of this integration the package was renamed to more +accurately reflect its status as a component of GPGME. The @samp{pyme3} +module was renamed to @samp{gpg} and adopted by the upstream GnuPG team. + +In 2017 Justus departed G10code and the GnuPG team. Following this +Ben returned to maintain of gpgme Python bindings and continue +building them from that point. + +@node Relics of the past +@chapter Relics of the past + +There are a few things, in addition to code specific factors, such as +SWIG itself, which are worth noting here. + +@menu +* The Annoyances of Git:: +* The Perils of PyPI:: +@end menu + +@node The Annoyances of Git +@section The Annoyances of Git + +As anyone who has ever worked with git knows, submodules are +horrible way to deal with pretty much anything. In the interests +of avoiding migraines, that was skipped with addition of the PyME +code to GPGME. + +Instead the files were added to a subdirectory of the @samp{lang/} +directory, along with a copy of the entire git log up to that point +as a separate file within the @samp{lang/python/docs/} directory.@footnote{The entire PyME git log and other preceding VCS logs are +located in the @samp{gpgme/lang/python/docs/old-commits.log} file.} +As the log for PyME is nearly 100KB and the log for GPGME is +approximately 1MB, this would cause considerable bloat, as well as +some confusion, should the two be merged. + +Hence the unfortunate, but necessary, step to simply move the +files. A regular repository version has been maintained should it +be possible to implement this better in the future. + +@node The Perils of PyPI +@section The Perils of PyPI + +The early port of the Python 2 @samp{pyme} module as @samp{pyme3} was never +added to PyPI while the focus remained on development and testing +during 2015 and early 2016. Later in 2016, however, when Justus +completed his major integration work and subsequently renamed the +module from @samp{pyme3} to @samp{gpg}, some prior releases were also +provided through PyPI. + +Since these bindings require a matching release of the GPGME +libraries in order to function, it was determined that there was +little benefit in also providing a copy through PyPI since anyone +obtaining the GPGME source code would obtain the Python bindings +source code at the same time. Whereas there was the potential to +sew confusion amongst Python users installing the module from PyPI, +only to discover that without the relevant C files, header files or +SWIG compiled binaries, the Python module did them little good. + +There are only two files on PyPI which might turn up in a search +for this module or a sample of its content: + +@enumerate +@item +gpg (1.8.0) - Python bindings for GPGME GnuPG cryptography library +@item +pyme (0.9.0) - Python support for GPGME GnuPG cryptography library +@end enumerate + +@menu +* GPG 1·8·0 - Python bindings for GPGME GnuPG cryptography library:: +* PyME 0·9·0 - Python support for GPGME GnuPG cryptography library:: +@end menu + +@node GPG 1·8·0 - Python bindings for GPGME GnuPG cryptography library +@subsection GPG 1·8·0 - Python bindings for GPGME GnuPG cryptography library + +This is the most recent version to reach PyPI and is the version +of the official Pyhon bindings which shipped with GPGME 1.8.0. If +you have GPGME 1.8.0 installed and @emph{only} 1.8.0 installed, then it +is probably safe to use this copy from PyPI. + +As there have been a lot of changes since the release of GPGME +1.8.0, the GnuPG Project recommends not using this version of the +module and instead installing the current version of GPGME along +with the Python bindings included with that package. + +@node PyME 0·9·0 - Python support for GPGME GnuPG cryptography library +@subsection PyME 0·9·0 - Python support for GPGME GnuPG cryptography library + +This is the last release of the PyME bindings maintained by Martin +Albrecht and is only compatible with Python 2, it will not work +with Python 3. This is the version of the software from which the +port from Python 2 to Python 3 code was made in 2015. + +Users of the more recent Python bindings will recognise numerous +points of similarity, but also significant differences. It is +likely that the more recent official bindings will feel "more +pythonic." + +For those using Python 2, there is essentially no harm in using +this module, but it may lack a number of more recent features +added to GPGME. + +@bye
\ No newline at end of file diff --git a/lang/python/doc/texinfo/texinfo.tex b/lang/python/doc/texinfo/texinfo.tex new file mode 100644 index 0000000..a5c849c --- /dev/null +++ b/lang/python/doc/texinfo/texinfo.tex @@ -0,0 +1,8962 @@ +% texinfo.tex -- TeX macros to handle Texinfo files. +% +% Load plain if necessary, i.e., if running under initex. +\expandafter\ifx\csname fmtname\endcsname\relax\input plain\fi +% +\def\texinfoversion{2007-12-02.17} +% +% Copyright (C) 1985, 1986, 1988, 1990, 1991, 1992, 1993, 1994, 1995, 2007, +% 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, +% 2007 Free Software Foundation, Inc. +% +% This texinfo.tex file is free software: you can redistribute it and/or +% modify it under the terms of the GNU General Public License as +% published by the Free Software Foundation, either version 3 of the +% License, or (at your option) any later version. +% +% This texinfo.tex file is distributed in the hope that it will be +% useful, but WITHOUT ANY WARRANTY; without even the implied warranty +% of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +% General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <https://www.gnu.org/licenses/>. +% +% As a special exception, when this file is read by TeX when processing +% a Texinfo source document, you may use the result without +% restriction. (This has been our intent since Texinfo was invented.) +% +% Please try the latest version of texinfo.tex before submitting bug +% reports; you can get the latest version from: +% https://www.gnu.org/software/texinfo/ (the Texinfo home page), or +% ftp://tug.org/tex/texinfo.tex +% (and all CTAN mirrors, see http://www.ctan.org). +% The texinfo.tex in any given distribution could well be out +% of date, so if that's what you're using, please check. +% +% Send bug reports to bug-texinfo@gnu.org. Please include including a +% complete document in each bug report with which we can reproduce the +% problem. Patches are, of course, greatly appreciated. +% +% To process a Texinfo manual with TeX, it's most reliable to use the +% texi2dvi shell script that comes with the distribution. For a simple +% manual foo.texi, however, you can get away with this: +% tex foo.texi +% texindex foo.?? +% tex foo.texi +% tex foo.texi +% dvips foo.dvi -o # or whatever; this makes foo.ps. +% The extra TeX runs get the cross-reference information correct. +% Sometimes one run after texindex suffices, and sometimes you need more +% than two; texi2dvi does it as many times as necessary. +% +% It is possible to adapt texinfo.tex for other languages, to some +% extent. You can get the existing language-specific files from the +% full Texinfo distribution. +% +% The GNU Texinfo home page is https://www.gnu.org/software/texinfo. + + +\message{Loading texinfo [version \texinfoversion]:} + +% If in a .fmt file, print the version number +% and turn on active characters that we couldn't do earlier because +% they might have appeared in the input file name. +\everyjob{\message{[Texinfo version \texinfoversion]}% + \catcode`+=\active \catcode`\_=\active} + + +\chardef\other=12 + +% We never want plain's \outer definition of \+ in Texinfo. +% For @tex, we can use \tabalign. +\let\+ = \relax + +% Save some plain tex macros whose names we will redefine. +\let\ptexb=\b +\let\ptexbullet=\bullet +\let\ptexc=\c +\let\ptexcomma=\, +\let\ptexdot=\. +\let\ptexdots=\dots +\let\ptexend=\end +\let\ptexequiv=\equiv +\let\ptexexclam=\! +\let\ptexfootnote=\footnote +\let\ptexgtr=> +\let\ptexhat=^ +\let\ptexi=\i +\let\ptexindent=\indent +\let\ptexinsert=\insert +\let\ptexlbrace=\{ +\let\ptexless=< +\let\ptexnewwrite\newwrite +\let\ptexnoindent=\noindent +\let\ptexplus=+ +\let\ptexrbrace=\} +\let\ptexslash=\/ +\let\ptexstar=\* +\let\ptext=\t + +% If this character appears in an error message or help string, it +% starts a new line in the output. +\newlinechar = `^^J + +% Use TeX 3.0's \inputlineno to get the line number, for better error +% messages, but if we're using an old version of TeX, don't do anything. +% +\ifx\inputlineno\thisisundefined + \let\linenumber = \empty % Pre-3.0. +\else + \def\linenumber{l.\the\inputlineno:\space} +\fi + +% Set up fixed words for English if not already set. +\ifx\putwordAppendix\undefined \gdef\putwordAppendix{Appendix}\fi +\ifx\putwordChapter\undefined \gdef\putwordChapter{Chapter}\fi +\ifx\putwordfile\undefined \gdef\putwordfile{file}\fi +\ifx\putwordin\undefined \gdef\putwordin{in}\fi +\ifx\putwordIndexIsEmpty\undefined \gdef\putwordIndexIsEmpty{(Index is empty)}\fi +\ifx\putwordIndexNonexistent\undefined \gdef\putwordIndexNonexistent{(Index is nonexistent)}\fi +\ifx\putwordInfo\undefined \gdef\putwordInfo{Info}\fi +\ifx\putwordInstanceVariableof\undefined \gdef\putwordInstanceVariableof{Instance Variable of}\fi +\ifx\putwordMethodon\undefined \gdef\putwordMethodon{Method on}\fi +\ifx\putwordNoTitle\undefined \gdef\putwordNoTitle{No Title}\fi +\ifx\putwordof\undefined \gdef\putwordof{of}\fi +\ifx\putwordon\undefined \gdef\putwordon{on}\fi +\ifx\putwordpage\undefined \gdef\putwordpage{page}\fi +\ifx\putwordsection\undefined \gdef\putwordsection{section}\fi +\ifx\putwordSection\undefined \gdef\putwordSection{Section}\fi +\ifx\putwordsee\undefined \gdef\putwordsee{see}\fi +\ifx\putwordSee\undefined \gdef\putwordSee{See}\fi +\ifx\putwordShortTOC\undefined \gdef\putwordShortTOC{Short Contents}\fi +\ifx\putwordTOC\undefined \gdef\putwordTOC{Table of Contents}\fi +% +\ifx\putwordMJan\undefined \gdef\putwordMJan{January}\fi +\ifx\putwordMFeb\undefined \gdef\putwordMFeb{February}\fi +\ifx\putwordMMar\undefined \gdef\putwordMMar{March}\fi +\ifx\putwordMApr\undefined \gdef\putwordMApr{April}\fi +\ifx\putwordMMay\undefined \gdef\putwordMMay{May}\fi +\ifx\putwordMJun\undefined \gdef\putwordMJun{June}\fi +\ifx\putwordMJul\undefined \gdef\putwordMJul{July}\fi +\ifx\putwordMAug\undefined \gdef\putwordMAug{August}\fi +\ifx\putwordMSep\undefined \gdef\putwordMSep{September}\fi +\ifx\putwordMOct\undefined \gdef\putwordMOct{October}\fi +\ifx\putwordMNov\undefined \gdef\putwordMNov{November}\fi +\ifx\putwordMDec\undefined \gdef\putwordMDec{December}\fi +% +\ifx\putwordDefmac\undefined \gdef\putwordDefmac{Macro}\fi +\ifx\putwordDefspec\undefined \gdef\putwordDefspec{Special Form}\fi +\ifx\putwordDefvar\undefined \gdef\putwordDefvar{Variable}\fi +\ifx\putwordDefopt\undefined \gdef\putwordDefopt{User Option}\fi +\ifx\putwordDeffunc\undefined \gdef\putwordDeffunc{Function}\fi + +% Since the category of space is not known, we have to be careful. +\chardef\spacecat = 10 +\def\spaceisspace{\catcode`\ =\spacecat} + +% sometimes characters are active, so we need control sequences. +\chardef\colonChar = `\: +\chardef\commaChar = `\, +\chardef\dashChar = `\- +\chardef\dotChar = `\. +\chardef\exclamChar= `\! +\chardef\lquoteChar= `\` +\chardef\questChar = `\? +\chardef\rquoteChar= `\' +\chardef\semiChar = `\; +\chardef\underChar = `\_ + +% Ignore a token. +% +\def\gobble#1{} + +% The following is used inside several \edef's. +\def\makecsname#1{\expandafter\noexpand\csname#1\endcsname} + +% Hyphenation fixes. +\hyphenation{ + Flor-i-da Ghost-script Ghost-view Mac-OS Post-Script + ap-pen-dix bit-map bit-maps + data-base data-bases eshell fall-ing half-way long-est man-u-script + man-u-scripts mini-buf-fer mini-buf-fers over-view par-a-digm + par-a-digms rath-er rec-tan-gu-lar ro-bot-ics se-vere-ly set-up spa-ces + spell-ing spell-ings + stand-alone strong-est time-stamp time-stamps which-ever white-space + wide-spread wrap-around +} + +% Margin to add to right of even pages, to left of odd pages. +\newdimen\bindingoffset +\newdimen\normaloffset +\newdimen\pagewidth \newdimen\pageheight + +% For a final copy, take out the rectangles +% that mark overfull boxes (in case you have decided +% that the text looks ok even though it passes the margin). +% +\def\finalout{\overfullrule=0pt} + +% @| inserts a changebar to the left of the current line. It should +% surround any changed text. This approach does *not* work if the +% change spans more than two lines of output. To handle that, we would +% have adopt a much more difficult approach (putting marks into the main +% vertical list for the beginning and end of each change). +% +\def\|{% + % \vadjust can only be used in horizontal mode. + \leavevmode + % + % Append this vertical mode material after the current line in the output. + \vadjust{% + % We want to insert a rule with the height and depth of the current + % leading; that is exactly what \strutbox is supposed to record. + \vskip-\baselineskip + % + % \vadjust-items are inserted at the left edge of the type. So + % the \llap here moves out into the left-hand margin. + \llap{% + % + % For a thicker or thinner bar, change the `1pt'. + \vrule height\baselineskip width1pt + % + % This is the space between the bar and the text. + \hskip 12pt + }% + }% +} + +% Sometimes it is convenient to have everything in the transcript file +% and nothing on the terminal. We don't just call \tracingall here, +% since that produces some useless output on the terminal. We also make +% some effort to order the tracing commands to reduce output in the log +% file; cf. trace.sty in LaTeX. +% +\def\gloggingall{\begingroup \globaldefs = 1 \loggingall \endgroup}% +\def\loggingall{% + \tracingstats2 + \tracingpages1 + \tracinglostchars2 % 2 gives us more in etex + \tracingparagraphs1 + \tracingoutput1 + \tracingmacros2 + \tracingrestores1 + \showboxbreadth\maxdimen \showboxdepth\maxdimen + \ifx\eTeXversion\undefined\else % etex gives us more logging + \tracingscantokens1 + \tracingifs1 + \tracinggroups1 + \tracingnesting2 + \tracingassigns1 + \fi + \tracingcommands3 % 3 gives us more in etex + \errorcontextlines16 +}% + +% add check for \lastpenalty to plain's definitions. If the last thing +% we did was a \nobreak, we don't want to insert more space. +% +\def\smallbreak{\ifnum\lastpenalty<10000\par\ifdim\lastskip<\smallskipamount + \removelastskip\penalty-50\smallskip\fi\fi} +\def\medbreak{\ifnum\lastpenalty<10000\par\ifdim\lastskip<\medskipamount + \removelastskip\penalty-100\medskip\fi\fi} +\def\bigbreak{\ifnum\lastpenalty<10000\par\ifdim\lastskip<\bigskipamount + \removelastskip\penalty-200\bigskip\fi\fi} + +% For @cropmarks command. +% Do @cropmarks to get crop marks. +% +\newif\ifcropmarks +\let\cropmarks = \cropmarkstrue +% +% Dimensions to add cropmarks at corners. +% Added by P. A. MacKay, 12 Nov. 1986 +% +\newdimen\outerhsize \newdimen\outervsize % set by the paper size routines +\newdimen\cornerlong \cornerlong=1pc +\newdimen\cornerthick \cornerthick=.3pt +\newdimen\topandbottommargin \topandbottommargin=.75in + +% Output a mark which sets \thischapter, \thissection and \thiscolor. +% We dump everything together because we only have one kind of mark. +% This works because we only use \botmark / \topmark, not \firstmark. +% +% A mark contains a subexpression of the \ifcase ... \fi construct. +% \get*marks macros below extract the needed part using \ifcase. +% +% Another complication is to let the user choose whether \thischapter +% (\thissection) refers to the chapter (section) in effect at the top +% of a page, or that at the bottom of a page. The solution is +% described on page 260 of The TeXbook. It involves outputting two +% marks for the sectioning macros, one before the section break, and +% one after. I won't pretend I can describe this better than DEK... +\def\domark{% + \toks0=\expandafter{\lastchapterdefs}% + \toks2=\expandafter{\lastsectiondefs}% + \toks4=\expandafter{\prevchapterdefs}% + \toks6=\expandafter{\prevsectiondefs}% + \toks8=\expandafter{\lastcolordefs}% + \mark{% + \the\toks0 \the\toks2 + \noexpand\or \the\toks4 \the\toks6 + \noexpand\else \the\toks8 + }% +} +% \topmark doesn't work for the very first chapter (after the title +% page or the contents), so we use \firstmark there -- this gets us +% the mark with the chapter defs, unless the user sneaks in, e.g., +% @setcolor (or @url, or @link, etc.) between @contents and the very +% first @chapter. +\def\gettopheadingmarks{% + \ifcase0\topmark\fi + \ifx\thischapter\empty \ifcase0\firstmark\fi \fi +} +\def\getbottomheadingmarks{\ifcase1\botmark\fi} +\def\getcolormarks{\ifcase2\topmark\fi} + +% Avoid "undefined control sequence" errors. +\def\lastchapterdefs{} +\def\lastsectiondefs{} +\def\prevchapterdefs{} +\def\prevsectiondefs{} +\def\lastcolordefs{} + +% Main output routine. +\chardef\PAGE = 255 +\output = {\onepageout{\pagecontents\PAGE}} + +\newbox\headlinebox +\newbox\footlinebox + +% \onepageout takes a vbox as an argument. Note that \pagecontents +% does insertions, but you have to call it yourself. +\def\onepageout#1{% + \ifcropmarks \hoffset=0pt \else \hoffset=\normaloffset \fi + % + \ifodd\pageno \advance\hoffset by \bindingoffset + \else \advance\hoffset by -\bindingoffset\fi + % + % Do this outside of the \shipout so @code etc. will be expanded in + % the headline as they should be, not taken literally (outputting ''code). + \ifodd\pageno \getoddheadingmarks \else \getevenheadingmarks \fi + \setbox\headlinebox = \vbox{\let\hsize=\pagewidth \makeheadline}% + \ifodd\pageno \getoddfootingmarks \else \getevenfootingmarks \fi + \setbox\footlinebox = \vbox{\let\hsize=\pagewidth \makefootline}% + % + {% + % Have to do this stuff outside the \shipout because we want it to + % take effect in \write's, yet the group defined by the \vbox ends + % before the \shipout runs. + % + \indexdummies % don't expand commands in the output. + \normalturnoffactive % \ in index entries must not stay \, e.g., if + % the page break happens to be in the middle of an example. + % We don't want .vr (or whatever) entries like this: + % \entry{{\tt \indexbackslash }acronym}{32}{\code {\acronym}} + % "\acronym" won't work when it's read back in; + % it needs to be + % {\code {{\tt \backslashcurfont }acronym} + \shipout\vbox{% + % Do this early so pdf references go to the beginning of the page. + \ifpdfmakepagedest \pdfdest name{\the\pageno} xyz\fi + % + \ifcropmarks \vbox to \outervsize\bgroup + \hsize = \outerhsize + \vskip-\topandbottommargin + \vtop to0pt{% + \line{\ewtop\hfil\ewtop}% + \nointerlineskip + \line{% + \vbox{\moveleft\cornerthick\nstop}% + \hfill + \vbox{\moveright\cornerthick\nstop}% + }% + \vss}% + \vskip\topandbottommargin + \line\bgroup + \hfil % center the page within the outer (page) hsize. + \ifodd\pageno\hskip\bindingoffset\fi + \vbox\bgroup + \fi + % + \unvbox\headlinebox + \pagebody{#1}% + \ifdim\ht\footlinebox > 0pt + % Only leave this space if the footline is nonempty. + % (We lessened \vsize for it in \oddfootingyyy.) + % The \baselineskip=24pt in plain's \makefootline has no effect. + \vskip 24pt + \unvbox\footlinebox + \fi + % + \ifcropmarks + \egroup % end of \vbox\bgroup + \hfil\egroup % end of (centering) \line\bgroup + \vskip\topandbottommargin plus1fill minus1fill + \boxmaxdepth = \cornerthick + \vbox to0pt{\vss + \line{% + \vbox{\moveleft\cornerthick\nsbot}% + \hfill + \vbox{\moveright\cornerthick\nsbot}% + }% + \nointerlineskip + \line{\ewbot\hfil\ewbot}% + }% + \egroup % \vbox from first cropmarks clause + \fi + }% end of \shipout\vbox + }% end of group with \indexdummies + \advancepageno + \ifnum\outputpenalty>-20000 \else\dosupereject\fi +} + +\newinsert\margin \dimen\margin=\maxdimen + +\def\pagebody#1{\vbox to\pageheight{\boxmaxdepth=\maxdepth #1}} +{\catcode`\@ =11 +\gdef\pagecontents#1{\ifvoid\topins\else\unvbox\topins\fi +% marginal hacks, juha@viisa.uucp (Juha Takala) +\ifvoid\margin\else % marginal info is present + \rlap{\kern\hsize\vbox to\z@{\kern1pt\box\margin \vss}}\fi +\dimen@=\dp#1\relax \unvbox#1\relax +\ifvoid\footins\else\vskip\skip\footins\footnoterule \unvbox\footins\fi +\ifr@ggedbottom \kern-\dimen@ \vfil \fi} +} + +% Here are the rules for the cropmarks. Note that they are +% offset so that the space between them is truly \outerhsize or \outervsize +% (P. A. MacKay, 12 November, 1986) +% +\def\ewtop{\vrule height\cornerthick depth0pt width\cornerlong} +\def\nstop{\vbox + {\hrule height\cornerthick depth\cornerlong width\cornerthick}} +\def\ewbot{\vrule height0pt depth\cornerthick width\cornerlong} +\def\nsbot{\vbox + {\hrule height\cornerlong depth\cornerthick width\cornerthick}} + +% Parse an argument, then pass it to #1. The argument is the rest of +% the input line (except we remove a trailing comment). #1 should be a +% macro which expects an ordinary undelimited TeX argument. +% +\def\parsearg{\parseargusing{}} +\def\parseargusing#1#2{% + \def\argtorun{#2}% + \begingroup + \obeylines + \spaceisspace + #1% + \parseargline\empty% Insert the \empty token, see \finishparsearg below. +} + +{\obeylines % + \gdef\parseargline#1^^M{% + \endgroup % End of the group started in \parsearg. + \argremovecomment #1\comment\ArgTerm% + }% +} + +% First remove any @comment, then any @c comment. +\def\argremovecomment#1\comment#2\ArgTerm{\argremovec #1\c\ArgTerm} +\def\argremovec#1\c#2\ArgTerm{\argcheckspaces#1\^^M\ArgTerm} + +% Each occurrence of `\^^M' or `<space>\^^M' is replaced by a single space. +% +% \argremovec might leave us with trailing space, e.g., +% @end itemize @c foo +% This space token undergoes the same procedure and is eventually removed +% by \finishparsearg. +% +\def\argcheckspaces#1\^^M{\argcheckspacesX#1\^^M \^^M} +\def\argcheckspacesX#1 \^^M{\argcheckspacesY#1\^^M} +\def\argcheckspacesY#1\^^M#2\^^M#3\ArgTerm{% + \def\temp{#3}% + \ifx\temp\empty + % Do not use \next, perhaps the caller of \parsearg uses it; reuse \temp: + \let\temp\finishparsearg + \else + \let\temp\argcheckspaces + \fi + % Put the space token in: + \temp#1 #3\ArgTerm +} + +% If a _delimited_ argument is enclosed in braces, they get stripped; so +% to get _exactly_ the rest of the line, we had to prevent such situation. +% We prepended an \empty token at the very beginning and we expand it now, +% just before passing the control to \argtorun. +% (Similarly, we have to think about #3 of \argcheckspacesY above: it is +% either the null string, or it ends with \^^M---thus there is no danger +% that a pair of braces would be stripped. +% +% But first, we have to remove the trailing space token. +% +\def\finishparsearg#1 \ArgTerm{\expandafter\argtorun\expandafter{#1}} + +% \parseargdef\foo{...} +% is roughly equivalent to +% \def\foo{\parsearg\Xfoo} +% \def\Xfoo#1{...} +% +% Actually, I use \csname\string\foo\endcsname, ie. \\foo, as it is my +% favourite TeX trick. --kasal, 16nov03 + +\def\parseargdef#1{% + \expandafter \doparseargdef \csname\string#1\endcsname #1% +} +\def\doparseargdef#1#2{% + \def#2{\parsearg#1}% + \def#1##1% +} + +% Several utility definitions with active space: +{ + \obeyspaces + \gdef\obeyedspace{ } + + % Make each space character in the input produce a normal interword + % space in the output. Don't allow a line break at this space, as this + % is used only in environments like @example, where each line of input + % should produce a line of output anyway. + % + \gdef\sepspaces{\obeyspaces\let =\tie} + + % If an index command is used in an @example environment, any spaces + % therein should become regular spaces in the raw index file, not the + % expansion of \tie (\leavevmode \penalty \@M \ ). + \gdef\unsepspaces{\let =\space} +} + + +\def\flushcr{\ifx\par\lisppar \def\next##1{}\else \let\next=\relax \fi \next} + +% Define the framework for environments in texinfo.tex. It's used like this: +% +% \envdef\foo{...} +% \def\Efoo{...} +% +% It's the responsibility of \envdef to insert \begingroup before the +% actual body; @end closes the group after calling \Efoo. \envdef also +% defines \thisenv, so the current environment is known; @end checks +% whether the environment name matches. The \checkenv macro can also be +% used to check whether the current environment is the one expected. +% +% Non-false conditionals (@iftex, @ifset) don't fit into this, so they +% are not treated as environments; they don't open a group. (The +% implementation of @end takes care not to call \endgroup in this +% special case.) + + +% At runtime, environments start with this: +\def\startenvironment#1{\begingroup\def\thisenv{#1}} +% initialize +\let\thisenv\empty + +% ... but they get defined via ``\envdef\foo{...}'': +\long\def\envdef#1#2{\def#1{\startenvironment#1#2}} +\def\envparseargdef#1#2{\parseargdef#1{\startenvironment#1#2}} + +% Check whether we're in the right environment: +\def\checkenv#1{% + \def\temp{#1}% + \ifx\thisenv\temp + \else + \badenverr + \fi +} + +% Environment mismatch, #1 expected: +\def\badenverr{% + \errhelp = \EMsimple + \errmessage{This command can appear only \inenvironment\temp, + not \inenvironment\thisenv}% +} +\def\inenvironment#1{% + \ifx#1\empty + out of any environment% + \else + in environment \expandafter\string#1% + \fi +} + +% @end foo executes the definition of \Efoo. +% But first, it executes a specialized version of \checkenv +% +\parseargdef\end{% + \if 1\csname iscond.#1\endcsname + \else + % The general wording of \badenverr may not be ideal, but... --kasal, 06nov03 + \expandafter\checkenv\csname#1\endcsname + \csname E#1\endcsname + \endgroup + \fi +} + +\newhelp\EMsimple{Press RETURN to continue.} + + +%% Simple single-character @ commands + +% @@ prints an @ +% Kludge this until the fonts are right (grr). +\def\@{{\tt\char64}} + +% This is turned off because it was never documented +% and you can use @w{...} around a quote to suppress ligatures. +%% Define @` and @' to be the same as ` and ' +%% but suppressing ligatures. +%\def\`{{`}} +%\def\'{{'}} + +% Used to generate quoted braces. +\def\mylbrace {{\tt\char123}} +\def\myrbrace {{\tt\char125}} +\let\{=\mylbrace +\let\}=\myrbrace +\begingroup + % Definitions to produce \{ and \} commands for indices, + % and @{ and @} for the aux/toc files. + \catcode`\{ = \other \catcode`\} = \other + \catcode`\[ = 1 \catcode`\] = 2 + \catcode`\! = 0 \catcode`\\ = \other + !gdef!lbracecmd[\{]% + !gdef!rbracecmd[\}]% + !gdef!lbraceatcmd[@{]% + !gdef!rbraceatcmd[@}]% +!endgroup + +% @comma{} to avoid , parsing problems. +\let\comma = , + +% Accents: @, @dotaccent @ringaccent @ubaraccent @udotaccent +% Others are defined by plain TeX: @` @' @" @^ @~ @= @u @v @H. +\let\, = \c +\let\dotaccent = \. +\def\ringaccent#1{{\accent23 #1}} +\let\tieaccent = \t +\let\ubaraccent = \b +\let\udotaccent = \d + +% Other special characters: @questiondown @exclamdown @ordf @ordm +% Plain TeX defines: @AA @AE @O @OE @L (plus lowercase versions) @ss. +\def\questiondown{?`} +\def\exclamdown{!`} +\def\ordf{\leavevmode\raise1ex\hbox{\selectfonts\lllsize \underbar{a}}} +\def\ordm{\leavevmode\raise1ex\hbox{\selectfonts\lllsize \underbar{o}}} + +% Dotless i and dotless j, used for accents. +\def\imacro{i} +\def\jmacro{j} +\def\dotless#1{% + \def\temp{#1}% + \ifx\temp\imacro \ptexi + \else\ifx\temp\jmacro \j + \else \errmessage{@dotless can be used only with i or j}% + \fi\fi +} + +% The \TeX{} logo, as in plain, but resetting the spacing so that a +% period following counts as ending a sentence. (Idea found in latex.) +% +\edef\TeX{\TeX \spacefactor=1000 } + +% @LaTeX{} logo. Not quite the same results as the definition in +% latex.ltx, since we use a different font for the raised A; it's most +% convenient for us to use an explicitly smaller font, rather than using +% the \scriptstyle font (since we don't reset \scriptstyle and +% \scriptscriptstyle). +% +\def\LaTeX{% + L\kern-.36em + {\setbox0=\hbox{T}% + \vbox to \ht0{\hbox{\selectfonts\lllsize A}\vss}}% + \kern-.15em + \TeX +} + +% Be sure we're in horizontal mode when doing a tie, since we make space +% equivalent to this in @example-like environments. Otherwise, a space +% at the beginning of a line will start with \penalty -- and +% since \penalty is valid in vertical mode, we'd end up putting the +% penalty on the vertical list instead of in the new paragraph. +{\catcode`@ = 11 + % Avoid using \@M directly, because that causes trouble + % if the definition is written into an index file. + \global\let\tiepenalty = \@M + \gdef\tie{\leavevmode\penalty\tiepenalty\ } +} + +% @: forces normal size whitespace following. +\def\:{\spacefactor=1000 } + +% @* forces a line break. +\def\*{\hfil\break\hbox{}\ignorespaces} + +% @/ allows a line break. +\let\/=\allowbreak + +% @. is an end-of-sentence period. +\def\.{.\spacefactor=\endofsentencespacefactor\space} + +% @! is an end-of-sentence bang. +\def\!{!\spacefactor=\endofsentencespacefactor\space} + +% @? is an end-of-sentence query. +\def\?{?\spacefactor=\endofsentencespacefactor\space} + +% @frenchspacing on|off says whether to put extra space after punctuation. +% +\def\onword{on} +\def\offword{off} +% +\parseargdef\frenchspacing{% + \def\temp{#1}% + \ifx\temp\onword \plainfrenchspacing + \else\ifx\temp\offword \plainnonfrenchspacing + \else + \errhelp = \EMsimple + \errmessage{Unknown @frenchspacing option `\temp', must be on/off}% + \fi\fi +} + +% @w prevents a word break. Without the \leavevmode, @w at the +% beginning of a paragraph, when TeX is still in vertical mode, would +% produce a whole line of output instead of starting the paragraph. +\def\w#1{\leavevmode\hbox{#1}} + +% @group ... @end group forces ... to be all on one page, by enclosing +% it in a TeX vbox. We use \vtop instead of \vbox to construct the box +% to keep its height that of a normal line. According to the rules for +% \topskip (p.114 of the TeXbook), the glue inserted is +% max (\topskip - \ht (first item), 0). If that height is large, +% therefore, no glue is inserted, and the space between the headline and +% the text is small, which looks bad. +% +% Another complication is that the group might be very large. This can +% cause the glue on the previous page to be unduly stretched, because it +% does not have much material. In this case, it's better to add an +% explicit \vfill so that the extra space is at the bottom. The +% threshold for doing this is if the group is more than \vfilllimit +% percent of a page (\vfilllimit can be changed inside of @tex). +% +\newbox\groupbox +\def\vfilllimit{0.7} +% +\envdef\group{% + \ifnum\catcode`\^^M=\active \else + \errhelp = \groupinvalidhelp + \errmessage{@group invalid in context where filling is enabled}% + \fi + \startsavinginserts + % + \setbox\groupbox = \vtop\bgroup + % Do @comment since we are called inside an environment such as + % @example, where each end-of-line in the input causes an + % end-of-line in the output. We don't want the end-of-line after + % the `@group' to put extra space in the output. Since @group + % should appear on a line by itself (according to the Texinfo + % manual), we don't worry about eating any user text. + \comment +} +% +% The \vtop produces a box with normal height and large depth; thus, TeX puts +% \baselineskip glue before it, and (when the next line of text is done) +% \lineskip glue after it. Thus, space below is not quite equal to space +% above. But it's pretty close. +\def\Egroup{% + % To get correct interline space between the last line of the group + % and the first line afterwards, we have to propagate \prevdepth. + \endgraf % Not \par, as it may have been set to \lisppar. + \global\dimen1 = \prevdepth + \egroup % End the \vtop. + % \dimen0 is the vertical size of the group's box. + \dimen0 = \ht\groupbox \advance\dimen0 by \dp\groupbox + % \dimen2 is how much space is left on the page (more or less). + \dimen2 = \pageheight \advance\dimen2 by -\pagetotal + % if the group doesn't fit on the current page, and it's a big big + % group, force a page break. + \ifdim \dimen0 > \dimen2 + \ifdim \pagetotal < \vfilllimit\pageheight + \page + \fi + \fi + \box\groupbox + \prevdepth = \dimen1 + \checkinserts +} +% +% TeX puts in an \escapechar (i.e., `@') at the beginning of the help +% message, so this ends up printing `@group can only ...'. +% +\newhelp\groupinvalidhelp{% +group can only be used in environments such as @example,^^J% +where each line of input produces a line of output.} + +% @need space-in-mils +% forces a page break if there is not space-in-mils remaining. + +\newdimen\mil \mil=0.001in + +% Old definition--didn't work. +%\parseargdef\need{\par % +%% This method tries to make TeX break the page naturally +%% if the depth of the box does not fit. +%{\baselineskip=0pt% +%\vtop to #1\mil{\vfil}\kern -#1\mil\nobreak +%\prevdepth=-1000pt +%}} + +\parseargdef\need{% + % Ensure vertical mode, so we don't make a big box in the middle of a + % paragraph. + \par + % + % If the @need value is less than one line space, it's useless. + \dimen0 = #1\mil + \dimen2 = \ht\strutbox + \advance\dimen2 by \dp\strutbox + \ifdim\dimen0 > \dimen2 + % + % Do a \strut just to make the height of this box be normal, so the + % normal leading is inserted relative to the preceding line. + % And a page break here is fine. + \vtop to #1\mil{\strut\vfil}% + % + % TeX does not even consider page breaks if a penalty added to the + % main vertical list is 10000 or more. But in order to see if the + % empty box we just added fits on the page, we must make it consider + % page breaks. On the other hand, we don't want to actually break the + % page after the empty box. So we use a penalty of 9999. + % + % There is an extremely small chance that TeX will actually break the + % page at this \penalty, if there are no other feasible breakpoints in + % sight. (If the user is using lots of big @group commands, which + % almost-but-not-quite fill up a page, TeX will have a hard time doing + % good page breaking, for example.) However, I could not construct an + % example where a page broke at this \penalty; if it happens in a real + % document, then we can reconsider our strategy. + \penalty9999 + % + % Back up by the size of the box, whether we did a page break or not. + \kern -#1\mil + % + % Do not allow a page break right after this kern. + \nobreak + \fi +} + +% @br forces paragraph break (and is undocumented). + +\let\br = \par + +% @page forces the start of a new page. +% +\def\page{\par\vfill\supereject} + +% @exdent text.... +% outputs text on separate line in roman font, starting at standard page margin + +% This records the amount of indent in the innermost environment. +% That's how much \exdent should take out. +\newskip\exdentamount + +% This defn is used inside fill environments such as @defun. +\parseargdef\exdent{\hfil\break\hbox{\kern -\exdentamount{\rm#1}}\hfil\break} + +% This defn is used inside nofill environments such as @example. +\parseargdef\nofillexdent{{\advance \leftskip by -\exdentamount + \leftline{\hskip\leftskip{\rm#1}}}} + +% @inmargin{WHICH}{TEXT} puts TEXT in the WHICH margin next to the current +% paragraph. For more general purposes, use the \margin insertion +% class. WHICH is `l' or `r'. +% +\newskip\inmarginspacing \inmarginspacing=1cm +\def\strutdepth{\dp\strutbox} +% +\def\doinmargin#1#2{\strut\vadjust{% + \nobreak + \kern-\strutdepth + \vtop to \strutdepth{% + \baselineskip=\strutdepth + \vss + % if you have multiple lines of stuff to put here, you'll need to + % make the vbox yourself of the appropriate size. + \ifx#1l% + \llap{\ignorespaces #2\hskip\inmarginspacing}% + \else + \rlap{\hskip\hsize \hskip\inmarginspacing \ignorespaces #2}% + \fi + \null + }% +}} +\def\inleftmargin{\doinmargin l} +\def\inrightmargin{\doinmargin r} +% +% @inmargin{TEXT [, RIGHT-TEXT]} +% (if RIGHT-TEXT is given, use TEXT for left page, RIGHT-TEXT for right; +% else use TEXT for both). +% +\def\inmargin#1{\parseinmargin #1,,\finish} +\def\parseinmargin#1,#2,#3\finish{% not perfect, but better than nothing. + \setbox0 = \hbox{\ignorespaces #2}% + \ifdim\wd0 > 0pt + \def\lefttext{#1}% have both texts + \def\righttext{#2}% + \else + \def\lefttext{#1}% have only one text + \def\righttext{#1}% + \fi + % + \ifodd\pageno + \def\temp{\inrightmargin\righttext}% odd page -> outside is right margin + \else + \def\temp{\inleftmargin\lefttext}% + \fi + \temp +} + +% @include file insert text of that file as input. +% +\def\include{\parseargusing\filenamecatcodes\includezzz} +\def\includezzz#1{% + \pushthisfilestack + \def\thisfile{#1}% + {% + \makevalueexpandable + \def\temp{\input #1 }% + \expandafter + }\temp + \popthisfilestack +} +\def\filenamecatcodes{% + \catcode`\\=\other + \catcode`~=\other + \catcode`^=\other + \catcode`_=\other + \catcode`|=\other + \catcode`<=\other + \catcode`>=\other + \catcode`+=\other + \catcode`-=\other +} + +\def\pushthisfilestack{% + \expandafter\pushthisfilestackX\popthisfilestack\StackTerm +} +\def\pushthisfilestackX{% + \expandafter\pushthisfilestackY\thisfile\StackTerm +} +\def\pushthisfilestackY #1\StackTerm #2\StackTerm {% + \gdef\popthisfilestack{\gdef\thisfile{#1}\gdef\popthisfilestack{#2}}% +} + +\def\popthisfilestack{\errthisfilestackempty} +\def\errthisfilestackempty{\errmessage{Internal error: + the stack of filenames is empty.}} + +\def\thisfile{} + +% @center line +% outputs that line, centered. +% +\parseargdef\center{% + \ifhmode + \let\next\centerH + \else + \let\next\centerV + \fi + \next{\hfil \ignorespaces#1\unskip \hfil}% +} +\def\centerH#1{% + {% + \hfil\break + \advance\hsize by -\leftskip + \advance\hsize by -\rightskip + \line{#1}% + \break + }% +} +\def\centerV#1{\line{\kern\leftskip #1\kern\rightskip}} + +% @sp n outputs n lines of vertical space + +\parseargdef\sp{\vskip #1\baselineskip} + +% @comment ...line which is ignored... +% @c is the same as @comment +% @ignore ... @end ignore is another way to write a comment + +\def\comment{\begingroup \catcode`\^^M=\other% +\catcode`\@=\other \catcode`\{=\other \catcode`\}=\other% +\commentxxx} +{\catcode`\^^M=\other \gdef\commentxxx#1^^M{\endgroup}} + +\let\c=\comment + +% @paragraphindent NCHARS +% We'll use ems for NCHARS, close enough. +% NCHARS can also be the word `asis' or `none'. +% We cannot feasibly implement @paragraphindent asis, though. +% +\def\asisword{asis} % no translation, these are keywords +\def\noneword{none} +% +\parseargdef\paragraphindent{% + \def\temp{#1}% + \ifx\temp\asisword + \else + \ifx\temp\noneword + \defaultparindent = 0pt + \else + \defaultparindent = #1em + \fi + \fi + \parindent = \defaultparindent +} + +% @exampleindent NCHARS +% We'll use ems for NCHARS like @paragraphindent. +% It seems @exampleindent asis isn't necessary, but +% I preserve it to make it similar to @paragraphindent. +\parseargdef\exampleindent{% + \def\temp{#1}% + \ifx\temp\asisword + \else + \ifx\temp\noneword + \lispnarrowing = 0pt + \else + \lispnarrowing = #1em + \fi + \fi +} + +% @firstparagraphindent WORD +% If WORD is `none', then suppress indentation of the first paragraph +% after a section heading. If WORD is `insert', then do indent at such +% paragraphs. +% +% The paragraph indentation is suppressed or not by calling +% \suppressfirstparagraphindent, which the sectioning commands do. +% We switch the definition of this back and forth according to WORD. +% By default, we suppress indentation. +% +\def\suppressfirstparagraphindent{\dosuppressfirstparagraphindent} +\def\insertword{insert} +% +\parseargdef\firstparagraphindent{% + \def\temp{#1}% + \ifx\temp\noneword + \let\suppressfirstparagraphindent = \dosuppressfirstparagraphindent + \else\ifx\temp\insertword + \let\suppressfirstparagraphindent = \relax + \else + \errhelp = \EMsimple + \errmessage{Unknown @firstparagraphindent option `\temp'}% + \fi\fi +} + +% Here is how we actually suppress indentation. Redefine \everypar to +% \kern backwards by \parindent, and then reset itself to empty. +% +% We also make \indent itself not actually do anything until the next +% paragraph. +% +\gdef\dosuppressfirstparagraphindent{% + \gdef\indent{% + \restorefirstparagraphindent + \indent + }% + \gdef\noindent{% + \restorefirstparagraphindent + \noindent + }% + \global\everypar = {% + \kern -\parindent + \restorefirstparagraphindent + }% +} + +\gdef\restorefirstparagraphindent{% + \global \let \indent = \ptexindent + \global \let \noindent = \ptexnoindent + \global \everypar = {}% +} + + +% @asis just yields its argument. Used with @table, for example. +% +\def\asis#1{#1} + +% @math outputs its argument in math mode. +% +% One complication: _ usually means subscripts, but it could also mean +% an actual _ character, as in @math{@var{some_variable} + 1}. So make +% _ active, and distinguish by seeing if the current family is \slfam, +% which is what @var uses. +{ + \catcode`\_ = \active + \gdef\mathunderscore{% + \catcode`\_=\active + \def_{\ifnum\fam=\slfam \_\else\sb\fi}% + } +} +% Another complication: we want \\ (and @\) to output a \ character. +% FYI, plain.tex uses \\ as a temporary control sequence (why?), but +% this is not advertised and we don't care. Texinfo does not +% otherwise define @\. +% +% The \mathchar is class=0=ordinary, family=7=ttfam, position=5C=\. +\def\mathbackslash{\ifnum\fam=\ttfam \mathchar"075C \else\backslash \fi} +% +\def\math{% + \tex + \mathunderscore + \let\\ = \mathbackslash + \mathactive + $\finishmath +} +\def\finishmath#1{#1$\endgroup} % Close the group opened by \tex. + +% Some active characters (such as <) are spaced differently in math. +% We have to reset their definitions in case the @math was an argument +% to a command which sets the catcodes (such as @item or @section). +% +{ + \catcode`^ = \active + \catcode`< = \active + \catcode`> = \active + \catcode`+ = \active + \gdef\mathactive{% + \let^ = \ptexhat + \let< = \ptexless + \let> = \ptexgtr + \let+ = \ptexplus + } +} + +% @bullet and @minus need the same treatment as @math, just above. +\def\bullet{$\ptexbullet$} +\def\minus{$-$} + +% @dots{} outputs an ellipsis using the current font. +% We do .5em per period so that it has the same spacing in the cm +% typewriter fonts as three actual period characters; on the other hand, +% in other typewriter fonts three periods are wider than 1.5em. So do +% whichever is larger. +% +\def\dots{% + \leavevmode + \setbox0=\hbox{...}% get width of three periods + \ifdim\wd0 > 1.5em + \dimen0 = \wd0 + \else + \dimen0 = 1.5em + \fi + \hbox to \dimen0{% + \hskip 0pt plus.25fil + .\hskip 0pt plus1fil + .\hskip 0pt plus1fil + .\hskip 0pt plus.5fil + }% +} + +% @enddots{} is an end-of-sentence ellipsis. +% +\def\enddots{% + \dots + \spacefactor=\endofsentencespacefactor +} + +% @comma{} is so commas can be inserted into text without messing up +% Texinfo's parsing. +% +\let\comma = , + +% @refill is a no-op. +\let\refill=\relax + +% If working on a large document in chapters, it is convenient to +% be able to disable indexing, cross-referencing, and contents, for test runs. +% This is done with @novalidate (before @setfilename). +% +\newif\iflinks \linkstrue % by default we want the aux files. +\let\novalidate = \linksfalse + +% @setfilename is done at the beginning of every texinfo file. +% So open here the files we need to have open while reading the input. +% This makes it possible to make a .fmt file for texinfo. +\def\setfilename{% + \fixbackslash % Turn off hack to swallow `\input texinfo'. + \iflinks + \tryauxfile + % Open the new aux file. TeX will close it automatically at exit. + \immediate\openout\auxfile=\jobname.aux + \fi % \openindices needs to do some work in any case. + \openindices + \let\setfilename=\comment % Ignore extra @setfilename cmds. + % + % If texinfo.cnf is present on the system, read it. + % Useful for site-wide @afourpaper, etc. + \openin 1 texinfo.cnf + \ifeof 1 \else \input texinfo.cnf \fi + \closein 1 + % + \comment % Ignore the actual filename. +} + +% Called from \setfilename. +% +\def\openindices{% + \newindex{cp}% + \newcodeindex{fn}% + \newcodeindex{vr}% + \newcodeindex{tp}% + \newcodeindex{ky}% + \newcodeindex{pg}% +} + +% @bye. +\outer\def\bye{\pagealignmacro\tracingstats=1\ptexend} + + +\message{pdf,} +% adobe `portable' document format +\newcount\tempnum +\newcount\lnkcount +\newtoks\filename +\newcount\filenamelength +\newcount\pgn +\newtoks\toksA +\newtoks\toksB +\newtoks\toksC +\newtoks\toksD +\newbox\boxA +\newcount\countA +\newif\ifpdf +\newif\ifpdfmakepagedest + +% when pdftex is run in dvi mode, \pdfoutput is defined (so \pdfoutput=1 +% can be set). So we test for \relax and 0 as well as \undefined, +% borrowed from ifpdf.sty. +\ifx\pdfoutput\undefined +\else + \ifx\pdfoutput\relax + \else + \ifcase\pdfoutput + \else + \pdftrue + \fi + \fi +\fi + +% PDF uses PostScript string constants for the names of xref targets, +% for display in the outlines, and in other places. Thus, we have to +% double any backslashes. Otherwise, a name like "\node" will be +% interpreted as a newline (\n), followed by o, d, e. Not good. +% http://www.ntg.nl/pipermail/ntg-pdftex/2004-July/000654.html +% (and related messages, the final outcome is that it is up to the TeX +% user to double the backslashes and otherwise make the string valid, so +% that's what we do). + +% double active backslashes. +% +{\catcode`\@=0 \catcode`\\=\active + @gdef@activebackslashdouble{% + @catcode`@\=@active + @let\=@doublebackslash} +} + +% To handle parens, we must adopt a different approach, since parens are +% not active characters. hyperref.dtx (which has the same problem as +% us) handles it with this amazing macro to replace tokens, with minor +% changes for Texinfo. It is included here under the GPL by permission +% from the author, Heiko Oberdiek. +% +% #1 is the tokens to replace. +% #2 is the replacement. +% #3 is the control sequence with the string. +% +\def\HyPsdSubst#1#2#3{% + \def\HyPsdReplace##1#1##2\END{% + ##1% + \ifx\\##2\\% + \else + #2% + \HyReturnAfterFi{% + \HyPsdReplace##2\END + }% + \fi + }% + \xdef#3{\expandafter\HyPsdReplace#3#1\END}% +} +\long\def\HyReturnAfterFi#1\fi{\fi#1} + +% #1 is a control sequence in which to do the replacements. +\def\backslashparens#1{% + \xdef#1{#1}% redefine it as its expansion; the definition is simply + % \lastnode when called from \setref -> \pdfmkdest. + \HyPsdSubst{(}{\realbackslash(}{#1}% + \HyPsdSubst{)}{\realbackslash)}{#1}% +} + +\newhelp\nopdfimagehelp{Texinfo supports .png, .jpg, .jpeg, and .pdf images +with PDF output, and none of those formats could be found. (.eps cannot +be supported due to the design of the PDF format; use regular TeX (DVI +output) for that.)} + +\ifpdf + % + % Color manipulation macros based on pdfcolor.tex. + \def\cmykDarkRed{0.28 1 1 0.35} + \def\cmykBlack{0 0 0 1} + % + \def\pdfsetcolor#1{\pdfliteral{#1 k}} + % Set color, and create a mark which defines \thiscolor accordingly, + % so that \makeheadline knows which color to restore. + \def\setcolor#1{% + \xdef\lastcolordefs{\gdef\noexpand\thiscolor{#1}}% + \domark + \pdfsetcolor{#1}% + } + % + \def\maincolor{\cmykBlack} + \pdfsetcolor{\maincolor} + \edef\thiscolor{\maincolor} + \def\lastcolordefs{} + % + \def\makefootline{% + \baselineskip24pt + \line{\pdfsetcolor{\maincolor}\the\footline}% + } + % + \def\makeheadline{% + \vbox to 0pt{% + \vskip-22.5pt + \line{% + \vbox to8.5pt{}% + % Extract \thiscolor definition from the marks. + \getcolormarks + % Typeset the headline with \maincolor, then restore the color. + \pdfsetcolor{\maincolor}\the\headline\pdfsetcolor{\thiscolor}% + }% + \vss + }% + \nointerlineskip + } + % + % + \pdfcatalog{/PageMode /UseOutlines} + % + % #1 is image name, #2 width (might be empty/whitespace), #3 height (ditto). + \def\dopdfimage#1#2#3{% + \def\imagewidth{#2}\setbox0 = \hbox{\ignorespaces #2}% + \def\imageheight{#3}\setbox2 = \hbox{\ignorespaces #3}% + % + % pdftex (and the PDF format) support .png, .jpg, .pdf (among + % others). Let's try in that order. + \let\pdfimgext=\empty + \begingroup + \openin 1 #1.png \ifeof 1 + \openin 1 #1.jpg \ifeof 1 + \openin 1 #1.jpeg \ifeof 1 + \openin 1 #1.JPG \ifeof 1 + \openin 1 #1.pdf \ifeof 1 + \errhelp = \nopdfimagehelp + \errmessage{Could not find image file #1 for pdf}% + \else \gdef\pdfimgext{pdf}% + \fi + \else \gdef\pdfimgext{JPG}% + \fi + \else \gdef\pdfimgext{jpeg}% + \fi + \else \gdef\pdfimgext{jpg}% + \fi + \else \gdef\pdfimgext{png}% + \fi + \closein 1 + \endgroup + % + % without \immediate, pdftex seg faults when the same image is + % included twice. (Version 3.14159-pre-1.0-unofficial-20010704.) + \ifnum\pdftexversion < 14 + \immediate\pdfimage + \else + \immediate\pdfximage + \fi + \ifdim \wd0 >0pt width \imagewidth \fi + \ifdim \wd2 >0pt height \imageheight \fi + \ifnum\pdftexversion<13 + #1.\pdfimgext + \else + {#1.\pdfimgext}% + \fi + \ifnum\pdftexversion < 14 \else + \pdfrefximage \pdflastximage + \fi} + % + \def\pdfmkdest#1{{% + % We have to set dummies so commands such as @code, and characters + % such as \, aren't expanded when present in a section title. + \indexnofonts + \turnoffactive + \activebackslashdouble + \makevalueexpandable + \def\pdfdestname{#1}% + \backslashparens\pdfdestname + \safewhatsit{\pdfdest name{\pdfdestname} xyz}% + }} + % + % used to mark target names; must be expandable. + \def\pdfmkpgn#1{#1} + % + % by default, use a color that is dark enough to print on paper as + % nearly black, but still distinguishable for online viewing. + \def\urlcolor{\cmykDarkRed} + \def\linkcolor{\cmykDarkRed} + \def\endlink{\setcolor{\maincolor}\pdfendlink} + % + % Adding outlines to PDF; macros for calculating structure of outlines + % come from Petr Olsak + \def\expnumber#1{\expandafter\ifx\csname#1\endcsname\relax 0% + \else \csname#1\endcsname \fi} + \def\advancenumber#1{\tempnum=\expnumber{#1}\relax + \advance\tempnum by 1 + \expandafter\xdef\csname#1\endcsname{\the\tempnum}} + % + % #1 is the section text, which is what will be displayed in the + % outline by the pdf viewer. #2 is the pdf expression for the number + % of subentries (or empty, for subsubsections). #3 is the node text, + % which might be empty if this toc entry had no corresponding node. + % #4 is the page number + % + \def\dopdfoutline#1#2#3#4{% + % Generate a link to the node text if that exists; else, use the + % page number. We could generate a destination for the section + % text in the case where a section has no node, but it doesn't + % seem worth the trouble, since most documents are normally structured. + \def\pdfoutlinedest{#3}% + \ifx\pdfoutlinedest\empty + \def\pdfoutlinedest{#4}% + \else + % Doubled backslashes in the name. + {\activebackslashdouble \xdef\pdfoutlinedest{#3}% + \backslashparens\pdfoutlinedest}% + \fi + % + % Also double the backslashes in the display string. + {\activebackslashdouble \xdef\pdfoutlinetext{#1}% + \backslashparens\pdfoutlinetext}% + % + \pdfoutline goto name{\pdfmkpgn{\pdfoutlinedest}}#2{\pdfoutlinetext}% + } + % + \def\pdfmakeoutlines{% + \begingroup + % Thanh's hack / proper braces in bookmarks + \edef\mylbrace{\iftrue \string{\else}\fi}\let\{=\mylbrace + \edef\myrbrace{\iffalse{\else\string}\fi}\let\}=\myrbrace + % + % Read toc silently, to get counts of subentries for \pdfoutline. + \def\numchapentry##1##2##3##4{% + \def\thischapnum{##2}% + \def\thissecnum{0}% + \def\thissubsecnum{0}% + }% + \def\numsecentry##1##2##3##4{% + \advancenumber{chap\thischapnum}% + \def\thissecnum{##2}% + \def\thissubsecnum{0}% + }% + \def\numsubsecentry##1##2##3##4{% + \advancenumber{sec\thissecnum}% + \def\thissubsecnum{##2}% + }% + \def\numsubsubsecentry##1##2##3##4{% + \advancenumber{subsec\thissubsecnum}% + }% + \def\thischapnum{0}% + \def\thissecnum{0}% + \def\thissubsecnum{0}% + % + % use \def rather than \let here because we redefine \chapentry et + % al. a second time, below. + \def\appentry{\numchapentry}% + \def\appsecentry{\numsecentry}% + \def\appsubsecentry{\numsubsecentry}% + \def\appsubsubsecentry{\numsubsubsecentry}% + \def\unnchapentry{\numchapentry}% + \def\unnsecentry{\numsecentry}% + \def\unnsubsecentry{\numsubsecentry}% + \def\unnsubsubsecentry{\numsubsubsecentry}% + \readdatafile{toc}% + % + % Read toc second time, this time actually producing the outlines. + % The `-' means take the \expnumber as the absolute number of + % subentries, which we calculated on our first read of the .toc above. + % + % We use the node names as the destinations. + \def\numchapentry##1##2##3##4{% + \dopdfoutline{##1}{count-\expnumber{chap##2}}{##3}{##4}}% + \def\numsecentry##1##2##3##4{% + \dopdfoutline{##1}{count-\expnumber{sec##2}}{##3}{##4}}% + \def\numsubsecentry##1##2##3##4{% + \dopdfoutline{##1}{count-\expnumber{subsec##2}}{##3}{##4}}% + \def\numsubsubsecentry##1##2##3##4{% count is always zero + \dopdfoutline{##1}{}{##3}{##4}}% + % + % PDF outlines are displayed using system fonts, instead of + % document fonts. Therefore we cannot use special characters, + % since the encoding is unknown. For example, the eogonek from + % Latin 2 (0xea) gets translated to a | character. Info from + % Staszek Wawrykiewicz, 19 Jan 2004 04:09:24 +0100. + % + % xx to do this right, we have to translate 8-bit characters to + % their "best" equivalent, based on the @documentencoding. Right + % now, I guess we'll just let the pdf reader have its way. + \indexnofonts + \setupdatafile + \catcode`\\=\active \otherbackslash + \input \tocreadfilename + \endgroup + } + % + \def\skipspaces#1{\def\PP{#1}\def\D{|}% + \ifx\PP\D\let\nextsp\relax + \else\let\nextsp\skipspaces + \ifx\p\space\else\addtokens{\filename}{\PP}% + \advance\filenamelength by 1 + \fi + \fi + \nextsp} + \def\getfilename#1{\filenamelength=0\expandafter\skipspaces#1|\relax} + \ifnum\pdftexversion < 14 + \let \startlink \pdfannotlink + \else + \let \startlink \pdfstartlink + \fi + % make a live url in pdf output. + \def\pdfurl#1{% + \begingroup + % it seems we really need yet another set of dummies; have not + % tried to figure out what each command should do in the context + % of @url. for now, just make @/ a no-op, that's the only one + % people have actually reported a problem with. + % + \normalturnoffactive + \def\@{@}% + \let\/=\empty + \makevalueexpandable + \leavevmode\setcolor{\urlcolor}% + \startlink attr{/Border [0 0 0]}% + user{/Subtype /Link /A << /S /URI /URI (#1) >>}% + \endgroup} + \def\pdfgettoks#1.{\setbox\boxA=\hbox{\toksA={#1.}\toksB={}\maketoks}} + \def\addtokens#1#2{\edef\addtoks{\noexpand#1={\the#1#2}}\addtoks} + \def\adn#1{\addtokens{\toksC}{#1}\global\countA=1\let\next=\maketoks} + \def\poptoks#1#2|ENDTOKS|{\let\first=#1\toksD={#1}\toksA={#2}} + \def\maketoks{% + \expandafter\poptoks\the\toksA|ENDTOKS|\relax + \ifx\first0\adn0 + \else\ifx\first1\adn1 \else\ifx\first2\adn2 \else\ifx\first3\adn3 + \else\ifx\first4\adn4 \else\ifx\first5\adn5 \else\ifx\first6\adn6 + \else\ifx\first7\adn7 \else\ifx\first8\adn8 \else\ifx\first9\adn9 + \else + \ifnum0=\countA\else\makelink\fi + \ifx\first.\let\next=\done\else + \let\next=\maketoks + \addtokens{\toksB}{\the\toksD} + \ifx\first,\addtokens{\toksB}{\space}\fi + \fi + \fi\fi\fi\fi\fi\fi\fi\fi\fi\fi + \next} + \def\makelink{\addtokens{\toksB}% + {\noexpand\pdflink{\the\toksC}}\toksC={}\global\countA=0} + \def\pdflink#1{% + \startlink attr{/Border [0 0 0]} goto name{\pdfmkpgn{#1}} + \setcolor{\linkcolor}#1\endlink} + \def\done{\edef\st{\global\noexpand\toksA={\the\toksB}}\st} +\else + \let\pdfmkdest = \gobble + \let\pdfurl = \gobble + \let\endlink = \relax + \let\setcolor = \gobble + \let\pdfsetcolor = \gobble + \let\pdfmakeoutlines = \relax +\fi % \ifx\pdfoutput + + +\message{fonts,} + +% Change the current font style to #1, remembering it in \curfontstyle. +% For now, we do not accumulate font styles: @b{@i{foo}} prints foo in +% italics, not bold italics. +% +\def\setfontstyle#1{% + \def\curfontstyle{#1}% not as a control sequence, because we are \edef'd. + \csname ten#1\endcsname % change the current font +} + +% Select #1 fonts with the current style. +% +\def\selectfonts#1{\csname #1fonts\endcsname \csname\curfontstyle\endcsname} + +\def\rm{\fam=0 \setfontstyle{rm}} +\def\it{\fam=\itfam \setfontstyle{it}} +\def\sl{\fam=\slfam \setfontstyle{sl}} +\def\bf{\fam=\bffam \setfontstyle{bf}}\def\bfstylename{bf} +\def\tt{\fam=\ttfam \setfontstyle{tt}} + +% Texinfo sort of supports the sans serif font style, which plain TeX does not. +% So we set up a \sf. +\newfam\sffam +\def\sf{\fam=\sffam \setfontstyle{sf}} +\let\li = \sf % Sometimes we call it \li, not \sf. + +% We don't need math for this font style. +\def\ttsl{\setfontstyle{ttsl}} + + +% Default leading. +\newdimen\textleading \textleading = 13.2pt + +% Set the baselineskip to #1, and the lineskip and strut size +% correspondingly. There is no deep meaning behind these magic numbers +% used as factors; they just match (closely enough) what Knuth defined. +% +\def\lineskipfactor{.08333} +\def\strutheightpercent{.70833} +\def\strutdepthpercent {.29167} +% +% can get a sort of poor man's double spacing by redefining this. +\def\baselinefactor{1} +% +\def\setleading#1{% + \dimen0 = #1\relax + \normalbaselineskip = \baselinefactor\dimen0 + \normallineskip = \lineskipfactor\normalbaselineskip + \normalbaselines + \setbox\strutbox =\hbox{% + \vrule width0pt height\strutheightpercent\baselineskip + depth \strutdepthpercent \baselineskip + }% +} + +% PDF CMaps. See also LaTeX's t1.cmap. +% +% do nothing with this by default. +\expandafter\let\csname cmapOT1\endcsname\gobble +\expandafter\let\csname cmapOT1IT\endcsname\gobble +\expandafter\let\csname cmapOT1TT\endcsname\gobble + +% if we are producing pdf, and we have \pdffontattr, then define cmaps. +% (\pdffontattr was introduced many years ago, but people still run +% older pdftex's; it's easy to conditionalize, so we do.) +\ifpdf \ifx\pdffontattr\undefined \else + \begingroup + \catcode`\^^M=\active \def^^M{^^J}% Output line endings as the ^^J char. + \catcode`\%=12 \immediate\pdfobj stream {%!PS-Adobe-3.0 Resource-CMap +%%DocumentNeededResources: ProcSet (CIDInit) +%%IncludeResource: ProcSet (CIDInit) +%%BeginResource: CMap (TeX-OT1-0) +%%Title: (TeX-OT1-0 TeX OT1 0) +%%Version: 1.000 +%%EndComments +/CIDInit /ProcSet findresource begin +12 dict begin +begincmap +/CIDSystemInfo +<< /Registry (TeX) +/Ordering (OT1) +/Supplement 0 +>> def +/CMapName /TeX-OT1-0 def +/CMapType 2 def +1 begincodespacerange +<00> <7F> +endcodespacerange +8 beginbfrange +<00> <01> <0393> +<09> <0A> <03A8> +<23> <26> <0023> +<28> <3B> <0028> +<3F> <5B> <003F> +<5D> <5E> <005D> +<61> <7A> <0061> +<7B> <7C> <2013> +endbfrange +40 beginbfchar +<02> <0398> +<03> <039B> +<04> <039E> +<05> <03A0> +<06> <03A3> +<07> <03D2> +<08> <03A6> +<0B> <00660066> +<0C> <00660069> +<0D> <0066006C> +<0E> <006600660069> +<0F> <00660066006C> +<10> <0131> +<11> <0237> +<12> <0060> +<13> <00B4> +<14> <02C7> +<15> <02D8> +<16> <00AF> +<17> <02DA> +<18> <00B8> +<19> <00DF> +<1A> <00E6> +<1B> <0153> +<1C> <00F8> +<1D> <00C6> +<1E> <0152> +<1F> <00D8> +<21> <0021> +<22> <201D> +<27> <2019> +<3C> <00A1> +<3D> <003D> +<3E> <00BF> +<5C> <201C> +<5F> <02D9> +<60> <2018> +<7D> <02DD> +<7E> <007E> +<7F> <00A8> +endbfchar +endcmap +CMapName currentdict /CMap defineresource pop +end +end +%%EndResource +%%EOF + }\endgroup + \expandafter\edef\csname cmapOT1\endcsname#1{% + \pdffontattr#1{/ToUnicode \the\pdflastobj\space 0 R}% + }% +% +% \cmapOT1IT + \begingroup + \catcode`\^^M=\active \def^^M{^^J}% Output line endings as the ^^J char. + \catcode`\%=12 \immediate\pdfobj stream {%!PS-Adobe-3.0 Resource-CMap +%%DocumentNeededResources: ProcSet (CIDInit) +%%IncludeResource: ProcSet (CIDInit) +%%BeginResource: CMap (TeX-OT1IT-0) +%%Title: (TeX-OT1IT-0 TeX OT1IT 0) +%%Version: 1.000 +%%EndComments +/CIDInit /ProcSet findresource begin +12 dict begin +begincmap +/CIDSystemInfo +<< /Registry (TeX) +/Ordering (OT1IT) +/Supplement 0 +>> def +/CMapName /TeX-OT1IT-0 def +/CMapType 2 def +1 begincodespacerange +<00> <7F> +endcodespacerange +8 beginbfrange +<00> <01> <0393> +<09> <0A> <03A8> +<25> <26> <0025> +<28> <3B> <0028> +<3F> <5B> <003F> +<5D> <5E> <005D> +<61> <7A> <0061> +<7B> <7C> <2013> +endbfrange +42 beginbfchar +<02> <0398> +<03> <039B> +<04> <039E> +<05> <03A0> +<06> <03A3> +<07> <03D2> +<08> <03A6> +<0B> <00660066> +<0C> <00660069> +<0D> <0066006C> +<0E> <006600660069> +<0F> <00660066006C> +<10> <0131> +<11> <0237> +<12> <0060> +<13> <00B4> +<14> <02C7> +<15> <02D8> +<16> <00AF> +<17> <02DA> +<18> <00B8> +<19> <00DF> +<1A> <00E6> +<1B> <0153> +<1C> <00F8> +<1D> <00C6> +<1E> <0152> +<1F> <00D8> +<21> <0021> +<22> <201D> +<23> <0023> +<24> <00A3> +<27> <2019> +<3C> <00A1> +<3D> <003D> +<3E> <00BF> +<5C> <201C> +<5F> <02D9> +<60> <2018> +<7D> <02DD> +<7E> <007E> +<7F> <00A8> +endbfchar +endcmap +CMapName currentdict /CMap defineresource pop +end +end +%%EndResource +%%EOF + }\endgroup + \expandafter\edef\csname cmapOT1IT\endcsname#1{% + \pdffontattr#1{/ToUnicode \the\pdflastobj\space 0 R}% + }% +% +% \cmapOT1TT + \begingroup + \catcode`\^^M=\active \def^^M{^^J}% Output line endings as the ^^J char. + \catcode`\%=12 \immediate\pdfobj stream {%!PS-Adobe-3.0 Resource-CMap +%%DocumentNeededResources: ProcSet (CIDInit) +%%IncludeResource: ProcSet (CIDInit) +%%BeginResource: CMap (TeX-OT1TT-0) +%%Title: (TeX-OT1TT-0 TeX OT1TT 0) +%%Version: 1.000 +%%EndComments +/CIDInit /ProcSet findresource begin +12 dict begin +begincmap +/CIDSystemInfo +<< /Registry (TeX) +/Ordering (OT1TT) +/Supplement 0 +>> def +/CMapName /TeX-OT1TT-0 def +/CMapType 2 def +1 begincodespacerange +<00> <7F> +endcodespacerange +5 beginbfrange +<00> <01> <0393> +<09> <0A> <03A8> +<21> <26> <0021> +<28> <5F> <0028> +<61> <7E> <0061> +endbfrange +32 beginbfchar +<02> <0398> +<03> <039B> +<04> <039E> +<05> <03A0> +<06> <03A3> +<07> <03D2> +<08> <03A6> +<0B> <2191> +<0C> <2193> +<0D> <0027> +<0E> <00A1> +<0F> <00BF> +<10> <0131> +<11> <0237> +<12> <0060> +<13> <00B4> +<14> <02C7> +<15> <02D8> +<16> <00AF> +<17> <02DA> +<18> <00B8> +<19> <00DF> +<1A> <00E6> +<1B> <0153> +<1C> <00F8> +<1D> <00C6> +<1E> <0152> +<1F> <00D8> +<20> <2423> +<27> <2019> +<60> <2018> +<7F> <00A8> +endbfchar +endcmap +CMapName currentdict /CMap defineresource pop +end +end +%%EndResource +%%EOF + }\endgroup + \expandafter\edef\csname cmapOT1TT\endcsname#1{% + \pdffontattr#1{/ToUnicode \the\pdflastobj\space 0 R}% + }% +\fi\fi + + +% Set the font macro #1 to the font named #2, adding on the +% specified font prefix (normally `cm'). +% #3 is the font's design size, #4 is a scale factor, #5 is the CMap +% encoding (currently only OT1, OT1IT and OT1TT are allowed, pass +% empty to omit). +\def\setfont#1#2#3#4#5{% + \font#1=\fontprefix#2#3 scaled #4 + \csname cmap#5\endcsname#1% +} +% This is what gets called when #5 of \setfont is empty. +\let\cmap\gobble +% emacs-page end of cmaps + +% Use cm as the default font prefix. +% To specify the font prefix, you must define \fontprefix +% before you read in texinfo.tex. +\ifx\fontprefix\undefined +\def\fontprefix{cm} +\fi +% Support font families that don't use the same naming scheme as CM. +\def\rmshape{r} +\def\rmbshape{bx} %where the normal face is bold +\def\bfshape{b} +\def\bxshape{bx} +\def\ttshape{tt} +\def\ttbshape{tt} +\def\ttslshape{sltt} +\def\itshape{ti} +\def\itbshape{bxti} +\def\slshape{sl} +\def\slbshape{bxsl} +\def\sfshape{ss} +\def\sfbshape{ss} +\def\scshape{csc} +\def\scbshape{csc} + +% Definitions for a main text size of 11pt. This is the default in +% Texinfo. +% +\def\definetextfontsizexi{% +% Text fonts (11.2pt, magstep1). +\def\textnominalsize{11pt} +\edef\mainmagstep{\magstephalf} +\setfont\textrm\rmshape{10}{\mainmagstep}{OT1} +\setfont\texttt\ttshape{10}{\mainmagstep}{OT1TT} +\setfont\textbf\bfshape{10}{\mainmagstep}{OT1} +\setfont\textit\itshape{10}{\mainmagstep}{OT1IT} +\setfont\textsl\slshape{10}{\mainmagstep}{OT1} +\setfont\textsf\sfshape{10}{\mainmagstep}{OT1} +\setfont\textsc\scshape{10}{\mainmagstep}{OT1} +\setfont\textttsl\ttslshape{10}{\mainmagstep}{OT1TT} +\font\texti=cmmi10 scaled \mainmagstep +\font\textsy=cmsy10 scaled \mainmagstep +\def\textecsize{1095} + +% A few fonts for @defun names and args. +\setfont\defbf\bfshape{10}{\magstep1}{OT1} +\setfont\deftt\ttshape{10}{\magstep1}{OT1TT} +\setfont\defttsl\ttslshape{10}{\magstep1}{OT1TT} +\def\df{\let\tentt=\deftt \let\tenbf = \defbf \let\tenttsl=\defttsl \bf} + +% Fonts for indices, footnotes, small examples (9pt). +\def\smallnominalsize{9pt} +\setfont\smallrm\rmshape{9}{1000}{OT1} +\setfont\smalltt\ttshape{9}{1000}{OT1TT} +\setfont\smallbf\bfshape{10}{900}{OT1} +\setfont\smallit\itshape{9}{1000}{OT1IT} +\setfont\smallsl\slshape{9}{1000}{OT1} +\setfont\smallsf\sfshape{9}{1000}{OT1} +\setfont\smallsc\scshape{10}{900}{OT1} +\setfont\smallttsl\ttslshape{10}{900}{OT1TT} +\font\smalli=cmmi9 +\font\smallsy=cmsy9 +\def\smallecsize{0900} + +% Fonts for small examples (8pt). +\def\smallernominalsize{8pt} +\setfont\smallerrm\rmshape{8}{1000}{OT1} +\setfont\smallertt\ttshape{8}{1000}{OT1TT} +\setfont\smallerbf\bfshape{10}{800}{OT1} +\setfont\smallerit\itshape{8}{1000}{OT1IT} +\setfont\smallersl\slshape{8}{1000}{OT1} +\setfont\smallersf\sfshape{8}{1000}{OT1} +\setfont\smallersc\scshape{10}{800}{OT1} +\setfont\smallerttsl\ttslshape{10}{800}{OT1TT} +\font\smalleri=cmmi8 +\font\smallersy=cmsy8 +\def\smallerecsize{0800} + +% Fonts for title page (20.4pt): +\def\titlenominalsize{20pt} +\setfont\titlerm\rmbshape{12}{\magstep3}{OT1} +\setfont\titleit\itbshape{10}{\magstep4}{OT1IT} +\setfont\titlesl\slbshape{10}{\magstep4}{OT1} +\setfont\titlett\ttbshape{12}{\magstep3}{OT1TT} +\setfont\titlettsl\ttslshape{10}{\magstep4}{OT1TT} +\setfont\titlesf\sfbshape{17}{\magstep1}{OT1} +\let\titlebf=\titlerm +\setfont\titlesc\scbshape{10}{\magstep4}{OT1} +\font\titlei=cmmi12 scaled \magstep3 +\font\titlesy=cmsy10 scaled \magstep4 +\def\authorrm{\secrm} +\def\authortt{\sectt} +\def\titleecsize{2074} + +% Chapter (and unnumbered) fonts (17.28pt). +\def\chapnominalsize{17pt} +\setfont\chaprm\rmbshape{12}{\magstep2}{OT1} +\setfont\chapit\itbshape{10}{\magstep3}{OT1IT} +\setfont\chapsl\slbshape{10}{\magstep3}{OT1} +\setfont\chaptt\ttbshape{12}{\magstep2}{OT1TT} +\setfont\chapttsl\ttslshape{10}{\magstep3}{OT1TT} +\setfont\chapsf\sfbshape{17}{1000}{OT1} +\let\chapbf=\chaprm +\setfont\chapsc\scbshape{10}{\magstep3}{OT1} +\font\chapi=cmmi12 scaled \magstep2 +\font\chapsy=cmsy10 scaled \magstep3 +\def\chapecsize{1728} + +% Section fonts (14.4pt). +\def\secnominalsize{14pt} +\setfont\secrm\rmbshape{12}{\magstep1}{OT1} +\setfont\secit\itbshape{10}{\magstep2}{OT1IT} +\setfont\secsl\slbshape{10}{\magstep2}{OT1} +\setfont\sectt\ttbshape{12}{\magstep1}{OT1TT} +\setfont\secttsl\ttslshape{10}{\magstep2}{OT1TT} +\setfont\secsf\sfbshape{12}{\magstep1}{OT1} +\let\secbf\secrm +\setfont\secsc\scbshape{10}{\magstep2}{OT1} +\font\seci=cmmi12 scaled \magstep1 +\font\secsy=cmsy10 scaled \magstep2 +\def\sececsize{1440} + +% Subsection fonts (13.15pt). +\def\ssecnominalsize{13pt} +\setfont\ssecrm\rmbshape{12}{\magstephalf}{OT1} +\setfont\ssecit\itbshape{10}{1315}{OT1IT} +\setfont\ssecsl\slbshape{10}{1315}{OT1} +\setfont\ssectt\ttbshape{12}{\magstephalf}{OT1TT} +\setfont\ssecttsl\ttslshape{10}{1315}{OT1TT} +\setfont\ssecsf\sfbshape{12}{\magstephalf}{OT1} +\let\ssecbf\ssecrm +\setfont\ssecsc\scbshape{10}{1315}{OT1} +\font\sseci=cmmi12 scaled \magstephalf +\font\ssecsy=cmsy10 scaled 1315 +\def\ssececsize{1200} + +% Reduced fonts for @acro in text (10pt). +\def\reducednominalsize{10pt} +\setfont\reducedrm\rmshape{10}{1000}{OT1} +\setfont\reducedtt\ttshape{10}{1000}{OT1TT} +\setfont\reducedbf\bfshape{10}{1000}{OT1} +\setfont\reducedit\itshape{10}{1000}{OT1IT} +\setfont\reducedsl\slshape{10}{1000}{OT1} +\setfont\reducedsf\sfshape{10}{1000}{OT1} +\setfont\reducedsc\scshape{10}{1000}{OT1} +\setfont\reducedttsl\ttslshape{10}{1000}{OT1TT} +\font\reducedi=cmmi10 +\font\reducedsy=cmsy10 +\def\reducedecsize{1000} + +% reset the current fonts +\textfonts +\rm +} % end of 11pt text font size definitions + + +% Definitions to make the main text be 10pt Computer Modern, with +% section, chapter, etc., sizes following suit. This is for the GNU +% Press printing of the Emacs 22 manual. Maybe other manuals in the +% future. Used with @smallbook, which sets the leading to 12pt. +% +\def\definetextfontsizex{% +% Text fonts (10pt). +\def\textnominalsize{10pt} +\edef\mainmagstep{1000} +\setfont\textrm\rmshape{10}{\mainmagstep}{OT1} +\setfont\texttt\ttshape{10}{\mainmagstep}{OT1TT} +\setfont\textbf\bfshape{10}{\mainmagstep}{OT1} +\setfont\textit\itshape{10}{\mainmagstep}{OT1IT} +\setfont\textsl\slshape{10}{\mainmagstep}{OT1} +\setfont\textsf\sfshape{10}{\mainmagstep}{OT1} +\setfont\textsc\scshape{10}{\mainmagstep}{OT1} +\setfont\textttsl\ttslshape{10}{\mainmagstep}{OT1TT} +\font\texti=cmmi10 scaled \mainmagstep +\font\textsy=cmsy10 scaled \mainmagstep +\def\textecsize{1000} + +% A few fonts for @defun names and args. +\setfont\defbf\bfshape{10}{\magstephalf}{OT1} +\setfont\deftt\ttshape{10}{\magstephalf}{OT1TT} +\setfont\defttsl\ttslshape{10}{\magstephalf}{OT1TT} +\def\df{\let\tentt=\deftt \let\tenbf = \defbf \let\tenttsl=\defttsl \bf} + +% Fonts for indices, footnotes, small examples (9pt). +\def\smallnominalsize{9pt} +\setfont\smallrm\rmshape{9}{1000}{OT1} +\setfont\smalltt\ttshape{9}{1000}{OT1TT} +\setfont\smallbf\bfshape{10}{900}{OT1} +\setfont\smallit\itshape{9}{1000}{OT1IT} +\setfont\smallsl\slshape{9}{1000}{OT1} +\setfont\smallsf\sfshape{9}{1000}{OT1} +\setfont\smallsc\scshape{10}{900}{OT1} +\setfont\smallttsl\ttslshape{10}{900}{OT1TT} +\font\smalli=cmmi9 +\font\smallsy=cmsy9 +\def\smallecsize{0900} + +% Fonts for small examples (8pt). +\def\smallernominalsize{8pt} +\setfont\smallerrm\rmshape{8}{1000}{OT1} +\setfont\smallertt\ttshape{8}{1000}{OT1TT} +\setfont\smallerbf\bfshape{10}{800}{OT1} +\setfont\smallerit\itshape{8}{1000}{OT1IT} +\setfont\smallersl\slshape{8}{1000}{OT1} +\setfont\smallersf\sfshape{8}{1000}{OT1} +\setfont\smallersc\scshape{10}{800}{OT1} +\setfont\smallerttsl\ttslshape{10}{800}{OT1TT} +\font\smalleri=cmmi8 +\font\smallersy=cmsy8 +\def\smallerecsize{0800} + +% Fonts for title page (20.4pt): +\def\titlenominalsize{20pt} +\setfont\titlerm\rmbshape{12}{\magstep3}{OT1} +\setfont\titleit\itbshape{10}{\magstep4}{OT1IT} +\setfont\titlesl\slbshape{10}{\magstep4}{OT1} +\setfont\titlett\ttbshape{12}{\magstep3}{OT1TT} +\setfont\titlettsl\ttslshape{10}{\magstep4}{OT1TT} +\setfont\titlesf\sfbshape{17}{\magstep1}{OT1} +\let\titlebf=\titlerm +\setfont\titlesc\scbshape{10}{\magstep4}{OT1} +\font\titlei=cmmi12 scaled \magstep3 +\font\titlesy=cmsy10 scaled \magstep4 +\def\authorrm{\secrm} +\def\authortt{\sectt} +\def\titleecsize{2074} + +% Chapter fonts (14.4pt). +\def\chapnominalsize{14pt} +\setfont\chaprm\rmbshape{12}{\magstep1}{OT1} +\setfont\chapit\itbshape{10}{\magstep2}{OT1IT} +\setfont\chapsl\slbshape{10}{\magstep2}{OT1} +\setfont\chaptt\ttbshape{12}{\magstep1}{OT1TT} +\setfont\chapttsl\ttslshape{10}{\magstep2}{OT1TT} +\setfont\chapsf\sfbshape{12}{\magstep1}{OT1} +\let\chapbf\chaprm +\setfont\chapsc\scbshape{10}{\magstep2}{OT1} +\font\chapi=cmmi12 scaled \magstep1 +\font\chapsy=cmsy10 scaled \magstep2 +\def\chapecsize{1440} + +% Section fonts (12pt). +\def\secnominalsize{12pt} +\setfont\secrm\rmbshape{12}{1000}{OT1} +\setfont\secit\itbshape{10}{\magstep1}{OT1IT} +\setfont\secsl\slbshape{10}{\magstep1}{OT1} +\setfont\sectt\ttbshape{12}{1000}{OT1TT} +\setfont\secttsl\ttslshape{10}{\magstep1}{OT1TT} +\setfont\secsf\sfbshape{12}{1000}{OT1} +\let\secbf\secrm +\setfont\secsc\scbshape{10}{\magstep1}{OT1} +\font\seci=cmmi12 +\font\secsy=cmsy10 scaled \magstep1 +\def\sececsize{1200} + +% Subsection fonts (10pt). +\def\ssecnominalsize{10pt} +\setfont\ssecrm\rmbshape{10}{1000}{OT1} +\setfont\ssecit\itbshape{10}{1000}{OT1IT} +\setfont\ssecsl\slbshape{10}{1000}{OT1} +\setfont\ssectt\ttbshape{10}{1000}{OT1TT} +\setfont\ssecttsl\ttslshape{10}{1000}{OT1TT} +\setfont\ssecsf\sfbshape{10}{1000}{OT1} +\let\ssecbf\ssecrm +\setfont\ssecsc\scbshape{10}{1000}{OT1} +\font\sseci=cmmi10 +\font\ssecsy=cmsy10 +\def\ssececsize{1000} + +% Reduced fonts for @acro in text (9pt). +\def\reducednominalsize{9pt} +\setfont\reducedrm\rmshape{9}{1000}{OT1} +\setfont\reducedtt\ttshape{9}{1000}{OT1TT} +\setfont\reducedbf\bfshape{10}{900}{OT1} +\setfont\reducedit\itshape{9}{1000}{OT1IT} +\setfont\reducedsl\slshape{9}{1000}{OT1} +\setfont\reducedsf\sfshape{9}{1000}{OT1} +\setfont\reducedsc\scshape{10}{900}{OT1} +\setfont\reducedttsl\ttslshape{10}{900}{OT1TT} +\font\reducedi=cmmi9 +\font\reducedsy=cmsy9 +\def\reducedecsize{0900} + +% reduce space between paragraphs +\divide\parskip by 2 + +% reset the current fonts +\textfonts +\rm +} % end of 10pt text font size definitions + + +% We provide the user-level command +% @fonttextsize 10 +% (or 11) to redefine the text font size. pt is assumed. +% +\def\xword{10} +\def\xiword{11} +% +\parseargdef\fonttextsize{% + \def\textsizearg{#1}% + \wlog{doing @fonttextsize \textsizearg}% + % + % Set \globaldefs so that documents can use this inside @tex, since + % makeinfo 4.8 does not support it, but we need it nonetheless. + % + \begingroup \globaldefs=1 + \ifx\textsizearg\xword \definetextfontsizex + \else \ifx\textsizearg\xiword \definetextfontsizexi + \else + \errhelp=\EMsimple + \errmessage{@fonttextsize only supports `10' or `11', not `\textsizearg'} + \fi\fi + \endgroup +} + + +% In order for the font changes to affect most math symbols and letters, +% we have to define the \textfont of the standard families. Since +% texinfo doesn't allow for producing subscripts and superscripts except +% in the main text, we don't bother to reset \scriptfont and +% \scriptscriptfont (which would also require loading a lot more fonts). +% +\def\resetmathfonts{% + \textfont0=\tenrm \textfont1=\teni \textfont2=\tensy + \textfont\itfam=\tenit \textfont\slfam=\tensl \textfont\bffam=\tenbf + \textfont\ttfam=\tentt \textfont\sffam=\tensf +} + +% The font-changing commands redefine the meanings of \tenSTYLE, instead +% of just \STYLE. We do this because \STYLE needs to also set the +% current \fam for math mode. Our \STYLE (e.g., \rm) commands hardwire +% \tenSTYLE to set the current font. +% +% Each font-changing command also sets the names \lsize (one size lower) +% and \lllsize (three sizes lower). These relative commands are used in +% the LaTeX logo and acronyms. +% +% This all needs generalizing, badly. +% +\def\textfonts{% + \let\tenrm=\textrm \let\tenit=\textit \let\tensl=\textsl + \let\tenbf=\textbf \let\tentt=\texttt \let\smallcaps=\textsc + \let\tensf=\textsf \let\teni=\texti \let\tensy=\textsy + \let\tenttsl=\textttsl + \def\curfontsize{text}% + \def\lsize{reduced}\def\lllsize{smaller}% + \resetmathfonts \setleading{\textleading}} +\def\titlefonts{% + \let\tenrm=\titlerm \let\tenit=\titleit \let\tensl=\titlesl + \let\tenbf=\titlebf \let\tentt=\titlett \let\smallcaps=\titlesc + \let\tensf=\titlesf \let\teni=\titlei \let\tensy=\titlesy + \let\tenttsl=\titlettsl + \def\curfontsize{title}% + \def\lsize{chap}\def\lllsize{subsec}% + \resetmathfonts \setleading{25pt}} +\def\titlefont#1{{\titlefonts\rm #1}} +\def\chapfonts{% + \let\tenrm=\chaprm \let\tenit=\chapit \let\tensl=\chapsl + \let\tenbf=\chapbf \let\tentt=\chaptt \let\smallcaps=\chapsc + \let\tensf=\chapsf \let\teni=\chapi \let\tensy=\chapsy + \let\tenttsl=\chapttsl + \def\curfontsize{chap}% + \def\lsize{sec}\def\lllsize{text}% + \resetmathfonts \setleading{19pt}} +\def\secfonts{% + \let\tenrm=\secrm \let\tenit=\secit \let\tensl=\secsl + \let\tenbf=\secbf \let\tentt=\sectt \let\smallcaps=\secsc + \let\tensf=\secsf \let\teni=\seci \let\tensy=\secsy + \let\tenttsl=\secttsl + \def\curfontsize{sec}% + \def\lsize{subsec}\def\lllsize{reduced}% + \resetmathfonts \setleading{16pt}} +\def\subsecfonts{% + \let\tenrm=\ssecrm \let\tenit=\ssecit \let\tensl=\ssecsl + \let\tenbf=\ssecbf \let\tentt=\ssectt \let\smallcaps=\ssecsc + \let\tensf=\ssecsf \let\teni=\sseci \let\tensy=\ssecsy + \let\tenttsl=\ssecttsl + \def\curfontsize{ssec}% + \def\lsize{text}\def\lllsize{small}% + \resetmathfonts \setleading{15pt}} +\let\subsubsecfonts = \subsecfonts +\def\reducedfonts{% + \let\tenrm=\reducedrm \let\tenit=\reducedit \let\tensl=\reducedsl + \let\tenbf=\reducedbf \let\tentt=\reducedtt \let\reducedcaps=\reducedsc + \let\tensf=\reducedsf \let\teni=\reducedi \let\tensy=\reducedsy + \let\tenttsl=\reducedttsl + \def\curfontsize{reduced}% + \def\lsize{small}\def\lllsize{smaller}% + \resetmathfonts \setleading{10.5pt}} +\def\smallfonts{% + \let\tenrm=\smallrm \let\tenit=\smallit \let\tensl=\smallsl + \let\tenbf=\smallbf \let\tentt=\smalltt \let\smallcaps=\smallsc + \let\tensf=\smallsf \let\teni=\smalli \let\tensy=\smallsy + \let\tenttsl=\smallttsl + \def\curfontsize{small}% + \def\lsize{smaller}\def\lllsize{smaller}% + \resetmathfonts \setleading{10.5pt}} +\def\smallerfonts{% + \let\tenrm=\smallerrm \let\tenit=\smallerit \let\tensl=\smallersl + \let\tenbf=\smallerbf \let\tentt=\smallertt \let\smallcaps=\smallersc + \let\tensf=\smallersf \let\teni=\smalleri \let\tensy=\smallersy + \let\tenttsl=\smallerttsl + \def\curfontsize{smaller}% + \def\lsize{smaller}\def\lllsize{smaller}% + \resetmathfonts \setleading{9.5pt}} + +% Set the fonts to use with the @small... environments. +\let\smallexamplefonts = \smallfonts + +% About \smallexamplefonts. If we use \smallfonts (9pt), @smallexample +% can fit this many characters: +% 8.5x11=86 smallbook=72 a4=90 a5=69 +% If we use \scriptfonts (8pt), then we can fit this many characters: +% 8.5x11=90+ smallbook=80 a4=90+ a5=77 +% For me, subjectively, the few extra characters that fit aren't worth +% the additional smallness of 8pt. So I'm making the default 9pt. +% +% By the way, for comparison, here's what fits with @example (10pt): +% 8.5x11=71 smallbook=60 a4=75 a5=58 +% +% I wish the USA used A4 paper. +% --karl, 24jan03. + + +% Set up the default fonts, so we can use them for creating boxes. +% +\definetextfontsizexi + +% Define these so they can be easily changed for other fonts. +\def\angleleft{$\langle$} +\def\angleright{$\rangle$} + +% Count depth in font-changes, for error checks +\newcount\fontdepth \fontdepth=0 + +% Fonts for short table of contents. +\setfont\shortcontrm\rmshape{12}{1000}{OT1} +\setfont\shortcontbf\bfshape{10}{\magstep1}{OT1} % no cmb12 +\setfont\shortcontsl\slshape{12}{1000}{OT1} +\setfont\shortconttt\ttshape{12}{1000}{OT1TT} + +%% Add scribe-like font environments, plus @l for inline lisp (usually sans +%% serif) and @ii for TeX italic + +% \smartitalic{ARG} outputs arg in italics, followed by an italic correction +% unless the following character is such as not to need one. +\def\smartitalicx{\ifx\next,\else\ifx\next-\else\ifx\next.\else + \ptexslash\fi\fi\fi} +\def\smartslanted#1{{\ifusingtt\ttsl\sl #1}\futurelet\next\smartitalicx} +\def\smartitalic#1{{\ifusingtt\ttsl\it #1}\futurelet\next\smartitalicx} + +% like \smartslanted except unconditionally uses \ttsl. +% @var is set to this for defun arguments. +\def\ttslanted#1{{\ttsl #1}\futurelet\next\smartitalicx} + +% like \smartslanted except unconditionally use \sl. We never want +% ttsl for book titles, do we? +\def\cite#1{{\sl #1}\futurelet\next\smartitalicx} + +\let\i=\smartitalic +\let\slanted=\smartslanted +\let\var=\smartslanted +\let\dfn=\smartslanted +\let\emph=\smartitalic + +% @b, explicit bold. +\def\b#1{{\bf #1}} +\let\strong=\b + +% @sansserif, explicit sans. +\def\sansserif#1{{\sf #1}} + +% We can't just use \exhyphenpenalty, because that only has effect at +% the end of a paragraph. Restore normal hyphenation at the end of the +% group within which \nohyphenation is presumably called. +% +\def\nohyphenation{\hyphenchar\font = -1 \aftergroup\restorehyphenation} +\def\restorehyphenation{\hyphenchar\font = `- } + +% Set sfcode to normal for the chars that usually have another value. +% Can't use plain's \frenchspacing because it uses the `\x notation, and +% sometimes \x has an active definition that messes things up. +% +\catcode`@=11 + \def\plainfrenchspacing{% + \sfcode\dotChar =\@m \sfcode\questChar=\@m \sfcode\exclamChar=\@m + \sfcode\colonChar=\@m \sfcode\semiChar =\@m \sfcode\commaChar =\@m + \def\endofsentencespacefactor{1000}% for @. and friends + } + \def\plainnonfrenchspacing{% + \sfcode`\.3000\sfcode`\?3000\sfcode`\!3000 + \sfcode`\:2000\sfcode`\;1500\sfcode`\,1250 + \def\endofsentencespacefactor{3000}% for @. and friends + } +\catcode`@=\other +\def\endofsentencespacefactor{3000}% default + +\def\t#1{% + {\tt \rawbackslash \plainfrenchspacing #1}% + \null +} +\def\samp#1{`\tclose{#1}'\null} +\setfont\keyrm\rmshape{8}{1000}{OT1} +\font\keysy=cmsy9 +\def\key#1{{\keyrm\textfont2=\keysy \leavevmode\hbox{% + \raise0.4pt\hbox{\angleleft}\kern-.08em\vtop{% + \vbox{\hrule\kern-0.4pt + \hbox{\raise0.4pt\hbox{\vphantom{\angleleft}}#1}}% + \kern-0.4pt\hrule}% + \kern-.06em\raise0.4pt\hbox{\angleright}}}} +\def\key #1{{\nohyphenation \uppercase{#1}}\null} +% The old definition, with no lozenge: +%\def\key #1{{\ttsl \nohyphenation \uppercase{#1}}\null} +\def\ctrl #1{{\tt \rawbackslash \hat}#1} + +% @file, @option are the same as @samp. +\let\file=\samp +\let\option=\samp + +% @code is a modification of @t, +% which makes spaces the same size as normal in the surrounding text. +\def\tclose#1{% + {% + % Change normal interword space to be same as for the current font. + \spaceskip = \fontdimen2\font + % + % Switch to typewriter. + \tt + % + % But `\ ' produces the large typewriter interword space. + \def\ {{\spaceskip = 0pt{} }}% + % + % Turn off hyphenation. + \nohyphenation + % + \rawbackslash + \plainfrenchspacing + #1% + }% + \null +} + +% We *must* turn on hyphenation at `-' and `_' in @code. +% Otherwise, it is too hard to avoid overfull hboxes +% in the Emacs manual, the Library manual, etc. + +% Unfortunately, TeX uses one parameter (\hyphenchar) to control +% both hyphenation at - and hyphenation within words. +% We must therefore turn them both off (\tclose does that) +% and arrange explicitly to hyphenate at a dash. +% -- rms. +{ + \catcode`\-=\active \catcode`\_=\active + \catcode`\'=\active \catcode`\`=\active + % + \global\def\code{\begingroup + \catcode\rquoteChar=\active \catcode\lquoteChar=\active + \let'\codequoteright \let`\codequoteleft + % + \catcode\dashChar=\active \catcode\underChar=\active + \ifallowcodebreaks + \let-\codedash + \let_\codeunder + \else + \let-\realdash + \let_\realunder + \fi + \codex + } +} + +\def\realdash{-} +\def\codedash{-\discretionary{}{}{}} +\def\codeunder{% + % this is all so @math{@code{var_name}+1} can work. In math mode, _ + % is "active" (mathcode"8000) and \normalunderscore (or \char95, etc.) + % will therefore expand the active definition of _, which is us + % (inside @code that is), therefore an endless loop. + \ifusingtt{\ifmmode + \mathchar"075F % class 0=ordinary, family 7=ttfam, pos 0x5F=_. + \else\normalunderscore \fi + \discretionary{}{}{}}% + {\_}% +} +\def\codex #1{\tclose{#1}\endgroup} + +% An additional complication: the above will allow breaks after, e.g., +% each of the four underscores in __typeof__. This is undesirable in +% some manuals, especially if they don't have long identifiers in +% general. @allowcodebreaks provides a way to control this. +% +\newif\ifallowcodebreaks \allowcodebreakstrue + +\def\keywordtrue{true} +\def\keywordfalse{false} + +\parseargdef\allowcodebreaks{% + \def\txiarg{#1}% + \ifx\txiarg\keywordtrue + \allowcodebreakstrue + \else\ifx\txiarg\keywordfalse + \allowcodebreaksfalse + \else + \errhelp = \EMsimple + \errmessage{Unknown @allowcodebreaks option `\txiarg'}% + \fi\fi +} + +% @kbd is like @code, except that if the argument is just one @key command, +% then @kbd has no effect. + +% @kbdinputstyle -- arg is `distinct' (@kbd uses slanted tty font always), +% `example' (@kbd uses ttsl only inside of @example and friends), +% or `code' (@kbd uses normal tty font always). +\parseargdef\kbdinputstyle{% + \def\txiarg{#1}% + \ifx\txiarg\worddistinct + \gdef\kbdexamplefont{\ttsl}\gdef\kbdfont{\ttsl}% + \else\ifx\txiarg\wordexample + \gdef\kbdexamplefont{\ttsl}\gdef\kbdfont{\tt}% + \else\ifx\txiarg\wordcode + \gdef\kbdexamplefont{\tt}\gdef\kbdfont{\tt}% + \else + \errhelp = \EMsimple + \errmessage{Unknown @kbdinputstyle option `\txiarg'}% + \fi\fi\fi +} +\def\worddistinct{distinct} +\def\wordexample{example} +\def\wordcode{code} + +% Default is `distinct.' +\kbdinputstyle distinct + +\def\xkey{\key} +\def\kbdfoo#1#2#3\par{\def\one{#1}\def\three{#3}\def\threex{??}% +\ifx\one\xkey\ifx\threex\three \key{#2}% +\else{\tclose{\kbdfont\look}}\fi +\else{\tclose{\kbdfont\look}}\fi} + +% For @indicateurl, @env, @command quotes seem unnecessary, so use \code. +\let\indicateurl=\code +\let\env=\code +\let\command=\code + +% @uref (abbreviation for `urlref') takes an optional (comma-separated) +% second argument specifying the text to display and an optional third +% arg as text to display instead of (rather than in addition to) the url +% itself. First (mandatory) arg is the url. Perhaps eventually put in +% a hypertex \special here. +% +\def\uref#1{\douref #1,,,\finish} +\def\douref#1,#2,#3,#4\finish{\begingroup + \unsepspaces + \pdfurl{#1}% + \setbox0 = \hbox{\ignorespaces #3}% + \ifdim\wd0 > 0pt + \unhbox0 % third arg given, show only that + \else + \setbox0 = \hbox{\ignorespaces #2}% + \ifdim\wd0 > 0pt + \ifpdf + \unhbox0 % PDF: 2nd arg given, show only it + \else + \unhbox0\ (\code{#1})% DVI: 2nd arg given, show both it and url + \fi + \else + \code{#1}% only url given, so show it + \fi + \fi + \endlink +\endgroup} + +% @url synonym for @uref, since that's how everyone uses it. +% +\let\url=\uref + +% rms does not like angle brackets --karl, 17may97. +% So now @email is just like @uref, unless we are pdf. +% +%\def\email#1{\angleleft{\tt #1}\angleright} +\ifpdf + \def\email#1{\doemail#1,,\finish} + \def\doemail#1,#2,#3\finish{\begingroup + \unsepspaces + \pdfurl{mailto:#1}% + \setbox0 = \hbox{\ignorespaces #2}% + \ifdim\wd0>0pt\unhbox0\else\code{#1}\fi + \endlink + \endgroup} +\else + \let\email=\uref +\fi + +% Check if we are currently using a typewriter font. Since all the +% Computer Modern typewriter fonts have zero interword stretch (and +% shrink), and it is reasonable to expect all typewriter fonts to have +% this property, we can check that font parameter. +% +\def\ifmonospace{\ifdim\fontdimen3\font=0pt } + +% Typeset a dimension, e.g., `in' or `pt'. The only reason for the +% argument is to make the input look right: @dmn{pt} instead of @dmn{}pt. +% +\def\dmn#1{\thinspace #1} + +\def\kbd#1{\def\look{#1}\expandafter\kbdfoo\look??\par} + +% @l was never documented to mean ``switch to the Lisp font'', +% and it is not used as such in any manual I can find. We need it for +% Polish suppressed-l. --karl, 22sep96. +%\def\l#1{{\li #1}\null} + +% Explicit font changes: @r, @sc, undocumented @ii. +\def\r#1{{\rm #1}} % roman font +\def\sc#1{{\smallcaps#1}} % smallcaps font +\def\ii#1{{\it #1}} % italic font + +% @acronym for "FBI", "NATO", and the like. +% We print this one point size smaller, since it's intended for +% all-uppercase. +% +\def\acronym#1{\doacronym #1,,\finish} +\def\doacronym#1,#2,#3\finish{% + {\selectfonts\lsize #1}% + \def\temp{#2}% + \ifx\temp\empty \else + \space ({\unsepspaces \ignorespaces \temp \unskip})% + \fi +} + +% @abbr for "Comput. J." and the like. +% No font change, but don't do end-of-sentence spacing. +% +\def\abbr#1{\doabbr #1,,\finish} +\def\doabbr#1,#2,#3\finish{% + {\plainfrenchspacing #1}% + \def\temp{#2}% + \ifx\temp\empty \else + \space ({\unsepspaces \ignorespaces \temp \unskip})% + \fi +} + +% @pounds{} is a sterling sign, which Knuth put in the CM italic font. +% +\def\pounds{{\it\$}} + +% @euro{} comes from a separate font, depending on the current style. +% We use the free feym* fonts from the eurosym package by Henrik +% Theiling, which support regular, slanted, bold and bold slanted (and +% "outlined" (blackboard board, sort of) versions, which we don't need). +% It is available from http://www.ctan.org/tex-archive/fonts/eurosym. +% +% Although only regular is the truly official Euro symbol, we ignore +% that. The Euro is designed to be slightly taller than the regular +% font height. +% +% feymr - regular +% feymo - slanted +% feybr - bold +% feybo - bold slanted +% +% There is no good (free) typewriter version, to my knowledge. +% A feymr10 euro is ~7.3pt wide, while a normal cmtt10 char is ~5.25pt wide. +% Hmm. +% +% Also doesn't work in math. Do we need to do math with euro symbols? +% Hope not. +% +% +\def\euro{{\eurofont e}} +\def\eurofont{% + % We set the font at each command, rather than predefining it in + % \textfonts and the other font-switching commands, so that + % installations which never need the symbol don't have to have the + % font installed. + % + % There is only one designed size (nominal 10pt), so we always scale + % that to the current nominal size. + % + % By the way, simply using "at 1em" works for cmr10 and the like, but + % does not work for cmbx10 and other extended/shrunken fonts. + % + \def\eurosize{\csname\curfontsize nominalsize\endcsname}% + % + \ifx\curfontstyle\bfstylename + % bold: + \font\thiseurofont = \ifusingit{feybo10}{feybr10} at \eurosize + \else + % regular: + \font\thiseurofont = \ifusingit{feymo10}{feymr10} at \eurosize + \fi + \thiseurofont +} + +% Hacks for glyphs from the EC fonts similar to \euro. We don't +% use \let for the aliases, because sometimes we redefine the original +% macro, and the alias should reflect the redefinition. +\def\guillemetleft{{\ecfont \char"13}} +\def\guillemotleft{\guillemetleft} +\def\guillemetright{{\ecfont \char"14}} +\def\guillemotright{\guillemetright} +\def\guilsinglleft{{\ecfont \char"0E}} +\def\guilsinglright{{\ecfont \char"0F}} +\def\quotedblbase{{\ecfont \char"12}} +\def\quotesinglbase{{\ecfont \char"0D}} +% +\def\ecfont{% + % We can't distinguish serif/sanserif and italic/slanted, but this + % is used for crude hacks anyway (like adding French and German + % quotes to documents typeset with CM, where we lose kerning), so + % hopefully nobody will notice/care. + \edef\ecsize{\csname\curfontsize ecsize\endcsname}% + \edef\nominalsize{\csname\curfontsize nominalsize\endcsname}% + \ifx\curfontstyle\bfstylename + % bold: + \font\thisecfont = ecb\ifusingit{i}{x}\ecsize \space at \nominalsize + \else + % regular: + \font\thisecfont = ec\ifusingit{ti}{rm}\ecsize \space at \nominalsize + \fi + \thisecfont +} + +% @registeredsymbol - R in a circle. The font for the R should really +% be smaller yet, but lllsize is the best we can do for now. +% Adapted from the plain.tex definition of \copyright. +% +\def\registeredsymbol{% + $^{{\ooalign{\hfil\raise.07ex\hbox{\selectfonts\lllsize R}% + \hfil\crcr\Orb}}% + }$% +} + +% @textdegree - the normal degrees sign. +% +\def\textdegree{$^\circ$} + +% Laurent Siebenmann reports \Orb undefined with: +% Textures 1.7.7 (preloaded format=plain 93.10.14) (68K) 16 APR 2004 02:38 +% so we'll define it if necessary. +% +\ifx\Orb\undefined +\def\Orb{\mathhexbox20D} +\fi + +% Quotes. +\chardef\quotedblleft="5C +\chardef\quotedblright=`\" +\chardef\quoteleft=`\` +\chardef\quoteright=`\' + + +\message{page headings,} + +\newskip\titlepagetopglue \titlepagetopglue = 1.5in +\newskip\titlepagebottomglue \titlepagebottomglue = 2pc + +% First the title page. Must do @settitle before @titlepage. +\newif\ifseenauthor +\newif\iffinishedtitlepage + +% Do an implicit @contents or @shortcontents after @end titlepage if the +% user says @setcontentsaftertitlepage or @setshortcontentsaftertitlepage. +% +\newif\ifsetcontentsaftertitlepage + \let\setcontentsaftertitlepage = \setcontentsaftertitlepagetrue +\newif\ifsetshortcontentsaftertitlepage + \let\setshortcontentsaftertitlepage = \setshortcontentsaftertitlepagetrue + +\parseargdef\shorttitlepage{\begingroup\hbox{}\vskip 1.5in \chaprm \centerline{#1}% + \endgroup\page\hbox{}\page} + +\envdef\titlepage{% + % Open one extra group, as we want to close it in the middle of \Etitlepage. + \begingroup + \parindent=0pt \textfonts + % Leave some space at the very top of the page. + \vglue\titlepagetopglue + % No rule at page bottom unless we print one at the top with @title. + \finishedtitlepagetrue + % + % Most title ``pages'' are actually two pages long, with space + % at the top of the second. We don't want the ragged left on the second. + \let\oldpage = \page + \def\page{% + \iffinishedtitlepage\else + \finishtitlepage + \fi + \let\page = \oldpage + \page + \null + }% +} + +\def\Etitlepage{% + \iffinishedtitlepage\else + \finishtitlepage + \fi + % It is important to do the page break before ending the group, + % because the headline and footline are only empty inside the group. + % If we use the new definition of \page, we always get a blank page + % after the title page, which we certainly don't want. + \oldpage + \endgroup + % + % Need this before the \...aftertitlepage checks so that if they are + % in effect the toc pages will come out with page numbers. + \HEADINGSon + % + % If they want short, they certainly want long too. + \ifsetshortcontentsaftertitlepage + \shortcontents + \contents + \global\let\shortcontents = \relax + \global\let\contents = \relax + \fi + % + \ifsetcontentsaftertitlepage + \contents + \global\let\contents = \relax + \global\let\shortcontents = \relax + \fi +} + +\def\finishtitlepage{% + \vskip4pt \hrule height 2pt width \hsize + \vskip\titlepagebottomglue + \finishedtitlepagetrue +} + +%%% Macros to be used within @titlepage: + +\let\subtitlerm=\tenrm +\def\subtitlefont{\subtitlerm \normalbaselineskip = 13pt \normalbaselines} + +\def\authorfont{\authorrm \normalbaselineskip = 16pt \normalbaselines + \let\tt=\authortt} + +\parseargdef\title{% + \checkenv\titlepage + \leftline{\titlefonts\rm #1} + % print a rule at the page bottom also. + \finishedtitlepagefalse + \vskip4pt \hrule height 4pt width \hsize \vskip4pt +} + +\parseargdef\subtitle{% + \checkenv\titlepage + {\subtitlefont \rightline{#1}}% +} + +% @author should come last, but may come many times. +% It can also be used inside @quotation. +% +\parseargdef\author{% + \def\temp{\quotation}% + \ifx\thisenv\temp + \def\quotationauthor{#1}% printed in \Equotation. + \else + \checkenv\titlepage + \ifseenauthor\else \vskip 0pt plus 1filll \seenauthortrue \fi + {\authorfont \leftline{#1}}% + \fi +} + + +%%% Set up page headings and footings. + +\let\thispage=\folio + +\newtoks\evenheadline % headline on even pages +\newtoks\oddheadline % headline on odd pages +\newtoks\evenfootline % footline on even pages +\newtoks\oddfootline % footline on odd pages + +% Now make TeX use those variables +\headline={{\textfonts\rm \ifodd\pageno \the\oddheadline + \else \the\evenheadline \fi}} +\footline={{\textfonts\rm \ifodd\pageno \the\oddfootline + \else \the\evenfootline \fi}\HEADINGShook} +\let\HEADINGShook=\relax + +% Commands to set those variables. +% For example, this is what @headings on does +% @evenheading @thistitle|@thispage|@thischapter +% @oddheading @thischapter|@thispage|@thistitle +% @evenfooting @thisfile|| +% @oddfooting ||@thisfile + + +\def\evenheading{\parsearg\evenheadingxxx} +\def\evenheadingxxx #1{\evenheadingyyy #1\|\|\|\|\finish} +\def\evenheadingyyy #1\|#2\|#3\|#4\finish{% +\global\evenheadline={\rlap{\centerline{#2}}\line{#1\hfil#3}}} + +\def\oddheading{\parsearg\oddheadingxxx} +\def\oddheadingxxx #1{\oddheadingyyy #1\|\|\|\|\finish} +\def\oddheadingyyy #1\|#2\|#3\|#4\finish{% +\global\oddheadline={\rlap{\centerline{#2}}\line{#1\hfil#3}}} + +\parseargdef\everyheading{\oddheadingxxx{#1}\evenheadingxxx{#1}}% + +\def\evenfooting{\parsearg\evenfootingxxx} +\def\evenfootingxxx #1{\evenfootingyyy #1\|\|\|\|\finish} +\def\evenfootingyyy #1\|#2\|#3\|#4\finish{% +\global\evenfootline={\rlap{\centerline{#2}}\line{#1\hfil#3}}} + +\def\oddfooting{\parsearg\oddfootingxxx} +\def\oddfootingxxx #1{\oddfootingyyy #1\|\|\|\|\finish} +\def\oddfootingyyy #1\|#2\|#3\|#4\finish{% + \global\oddfootline = {\rlap{\centerline{#2}}\line{#1\hfil#3}}% + % + % Leave some space for the footline. Hopefully ok to assume + % @evenfooting will not be used by itself. + \global\advance\pageheight by -12pt + \global\advance\vsize by -12pt +} + +\parseargdef\everyfooting{\oddfootingxxx{#1}\evenfootingxxx{#1}} + +% @evenheadingmarks top \thischapter <- chapter at the top of a page +% @evenheadingmarks bottom \thischapter <- chapter at the bottom of a page +% +% The same set of arguments for: +% +% @oddheadingmarks +% @evenfootingmarks +% @oddfootingmarks +% @everyheadingmarks +% @everyfootingmarks + +\def\evenheadingmarks{\headingmarks{even}{heading}} +\def\oddheadingmarks{\headingmarks{odd}{heading}} +\def\evenfootingmarks{\headingmarks{even}{footing}} +\def\oddfootingmarks{\headingmarks{odd}{footing}} +\def\everyheadingmarks#1 {\headingmarks{even}{heading}{#1} + \headingmarks{odd}{heading}{#1} } +\def\everyfootingmarks#1 {\headingmarks{even}{footing}{#1} + \headingmarks{odd}{footing}{#1} } +% #1 = even/odd, #2 = heading/footing, #3 = top/bottom. +\def\headingmarks#1#2#3 {% + \expandafter\let\expandafter\temp \csname get#3headingmarks\endcsname + \global\expandafter\let\csname get#1#2marks\endcsname \temp +} + +\everyheadingmarks bottom +\everyfootingmarks bottom + +% @headings double turns headings on for double-sided printing. +% @headings single turns headings on for single-sided printing. +% @headings off turns them off. +% @headings on same as @headings double, retained for compatibility. +% @headings after turns on double-sided headings after this page. +% @headings doubleafter turns on double-sided headings after this page. +% @headings singleafter turns on single-sided headings after this page. +% By default, they are off at the start of a document, +% and turned `on' after @end titlepage. + +\def\headings #1 {\csname HEADINGS#1\endcsname} + +\def\HEADINGSoff{% +\global\evenheadline={\hfil} \global\evenfootline={\hfil} +\global\oddheadline={\hfil} \global\oddfootline={\hfil}} +\HEADINGSoff +% When we turn headings on, set the page number to 1. +% For double-sided printing, put current file name in lower left corner, +% chapter name on inside top of right hand pages, document +% title on inside top of left hand pages, and page numbers on outside top +% edge of all pages. +\def\HEADINGSdouble{% +\global\pageno=1 +\global\evenfootline={\hfil} +\global\oddfootline={\hfil} +\global\evenheadline={\line{\folio\hfil\thistitle}} +\global\oddheadline={\line{\thischapter\hfil\folio}} +\global\let\contentsalignmacro = \chapoddpage +} +\let\contentsalignmacro = \chappager + +% For single-sided printing, chapter title goes across top left of page, +% page number on top right. +\def\HEADINGSsingle{% +\global\pageno=1 +\global\evenfootline={\hfil} +\global\oddfootline={\hfil} +\global\evenheadline={\line{\thischapter\hfil\folio}} +\global\oddheadline={\line{\thischapter\hfil\folio}} +\global\let\contentsalignmacro = \chappager +} +\def\HEADINGSon{\HEADINGSdouble} + +\def\HEADINGSafter{\let\HEADINGShook=\HEADINGSdoublex} +\let\HEADINGSdoubleafter=\HEADINGSafter +\def\HEADINGSdoublex{% +\global\evenfootline={\hfil} +\global\oddfootline={\hfil} +\global\evenheadline={\line{\folio\hfil\thistitle}} +\global\oddheadline={\line{\thischapter\hfil\folio}} +\global\let\contentsalignmacro = \chapoddpage +} + +\def\HEADINGSsingleafter{\let\HEADINGShook=\HEADINGSsinglex} +\def\HEADINGSsinglex{% +\global\evenfootline={\hfil} +\global\oddfootline={\hfil} +\global\evenheadline={\line{\thischapter\hfil\folio}} +\global\oddheadline={\line{\thischapter\hfil\folio}} +\global\let\contentsalignmacro = \chappager +} + +% Subroutines used in generating headings +% This produces Day Month Year style of output. +% Only define if not already defined, in case a txi-??.tex file has set +% up a different format (e.g., txi-cs.tex does this). +\ifx\today\undefined +\def\today{% + \number\day\space + \ifcase\month + \or\putwordMJan\or\putwordMFeb\or\putwordMMar\or\putwordMApr + \or\putwordMMay\or\putwordMJun\or\putwordMJul\or\putwordMAug + \or\putwordMSep\or\putwordMOct\or\putwordMNov\or\putwordMDec + \fi + \space\number\year} +\fi + +% @settitle line... specifies the title of the document, for headings. +% It generates no output of its own. +\def\thistitle{\putwordNoTitle} +\def\settitle{\parsearg{\gdef\thistitle}} + + +\message{tables,} +% Tables -- @table, @ftable, @vtable, @item(x). + +% default indentation of table text +\newdimen\tableindent \tableindent=.8in +% default indentation of @itemize and @enumerate text +\newdimen\itemindent \itemindent=.3in +% margin between end of table item and start of table text. +\newdimen\itemmargin \itemmargin=.1in + +% used internally for \itemindent minus \itemmargin +\newdimen\itemmax + +% Note @table, @ftable, and @vtable define @item, @itemx, etc., with +% these defs. +% They also define \itemindex +% to index the item name in whatever manner is desired (perhaps none). + +\newif\ifitemxneedsnegativevskip + +\def\itemxpar{\par\ifitemxneedsnegativevskip\nobreak\vskip-\parskip\nobreak\fi} + +\def\internalBitem{\smallbreak \parsearg\itemzzz} +\def\internalBitemx{\itemxpar \parsearg\itemzzz} + +\def\itemzzz #1{\begingroup % + \advance\hsize by -\rightskip + \advance\hsize by -\tableindent + \setbox0=\hbox{\itemindicate{#1}}% + \itemindex{#1}% + \nobreak % This prevents a break before @itemx. + % + % If the item text does not fit in the space we have, put it on a line + % by itself, and do not allow a page break either before or after that + % line. We do not start a paragraph here because then if the next + % command is, e.g., @kindex, the whatsit would get put into the + % horizontal list on a line by itself, resulting in extra blank space. + \ifdim \wd0>\itemmax + % + % Make this a paragraph so we get the \parskip glue and wrapping, + % but leave it ragged-right. + \begingroup + \advance\leftskip by-\tableindent + \advance\hsize by\tableindent + \advance\rightskip by0pt plus1fil + \leavevmode\unhbox0\par + \endgroup + % + % We're going to be starting a paragraph, but we don't want the + % \parskip glue -- logically it's part of the @item we just started. + \nobreak \vskip-\parskip + % + % Stop a page break at the \parskip glue coming up. However, if + % what follows is an environment such as @example, there will be no + % \parskip glue; then the negative vskip we just inserted would + % cause the example and the item to crash together. So we use this + % bizarre value of 10001 as a signal to \aboveenvbreak to insert + % \parskip glue after all. Section titles are handled this way also. + % + \penalty 10001 + \endgroup + \itemxneedsnegativevskipfalse + \else + % The item text fits into the space. Start a paragraph, so that the + % following text (if any) will end up on the same line. + \noindent + % Do this with kerns and \unhbox so that if there is a footnote in + % the item text, it can migrate to the main vertical list and + % eventually be printed. + \nobreak\kern-\tableindent + \dimen0 = \itemmax \advance\dimen0 by \itemmargin \advance\dimen0 by -\wd0 + \unhbox0 + \nobreak\kern\dimen0 + \endgroup + \itemxneedsnegativevskiptrue + \fi +} + +\def\item{\errmessage{@item while not in a list environment}} +\def\itemx{\errmessage{@itemx while not in a list environment}} + +% @table, @ftable, @vtable. +\envdef\table{% + \let\itemindex\gobble + \tablecheck{table}% +} +\envdef\ftable{% + \def\itemindex ##1{\doind {fn}{\code{##1}}}% + \tablecheck{ftable}% +} +\envdef\vtable{% + \def\itemindex ##1{\doind {vr}{\code{##1}}}% + \tablecheck{vtable}% +} +\def\tablecheck#1{% + \ifnum \the\catcode`\^^M=\active + \endgroup + \errmessage{This command won't work in this context; perhaps the problem is + that we are \inenvironment\thisenv}% + \def\next{\doignore{#1}}% + \else + \let\next\tablex + \fi + \next +} +\def\tablex#1{% + \def\itemindicate{#1}% + \parsearg\tabley +} +\def\tabley#1{% + {% + \makevalueexpandable + \edef\temp{\noexpand\tablez #1\space\space\space}% + \expandafter + }\temp \endtablez +} +\def\tablez #1 #2 #3 #4\endtablez{% + \aboveenvbreak + \ifnum 0#1>0 \advance \leftskip by #1\mil \fi + \ifnum 0#2>0 \tableindent=#2\mil \fi + \ifnum 0#3>0 \advance \rightskip by #3\mil \fi + \itemmax=\tableindent + \advance \itemmax by -\itemmargin + \advance \leftskip by \tableindent + \exdentamount=\tableindent + \parindent = 0pt + \parskip = \smallskipamount + \ifdim \parskip=0pt \parskip=2pt \fi + \let\item = \internalBitem + \let\itemx = \internalBitemx +} +\def\Etable{\endgraf\afterenvbreak} +\let\Eftable\Etable +\let\Evtable\Etable +\let\Eitemize\Etable +\let\Eenumerate\Etable + +% This is the counter used by @enumerate, which is really @itemize + +\newcount \itemno + +\envdef\itemize{\parsearg\doitemize} + +\def\doitemize#1{% + \aboveenvbreak + \itemmax=\itemindent + \advance\itemmax by -\itemmargin + \advance\leftskip by \itemindent + \exdentamount=\itemindent + \parindent=0pt + \parskip=\smallskipamount + \ifdim\parskip=0pt \parskip=2pt \fi + \def\itemcontents{#1}% + % @itemize with no arg is equivalent to @itemize @bullet. + \ifx\itemcontents\empty\def\itemcontents{\bullet}\fi + \let\item=\itemizeitem +} + +% Definition of @item while inside @itemize and @enumerate. +% +\def\itemizeitem{% + \advance\itemno by 1 % for enumerations + {\let\par=\endgraf \smallbreak}% reasonable place to break + {% + % If the document has an @itemize directly after a section title, a + % \nobreak will be last on the list, and \sectionheading will have + % done a \vskip-\parskip. In that case, we don't want to zero + % parskip, or the item text will crash with the heading. On the + % other hand, when there is normal text preceding the item (as there + % usually is), we do want to zero parskip, or there would be too much + % space. In that case, we won't have a \nobreak before. At least + % that's the theory. + \ifnum\lastpenalty<10000 \parskip=0in \fi + \noindent + \hbox to 0pt{\hss \itemcontents \kern\itemmargin}% + \vadjust{\penalty 1200}}% not good to break after first line of item. + \flushcr +} + +% \splitoff TOKENS\endmark defines \first to be the first token in +% TOKENS, and \rest to be the remainder. +% +\def\splitoff#1#2\endmark{\def\first{#1}\def\rest{#2}}% + +% Allow an optional argument of an uppercase letter, lowercase letter, +% or number, to specify the first label in the enumerated list. No +% argument is the same as `1'. +% +\envparseargdef\enumerate{\enumeratey #1 \endenumeratey} +\def\enumeratey #1 #2\endenumeratey{% + % If we were given no argument, pretend we were given `1'. + \def\thearg{#1}% + \ifx\thearg\empty \def\thearg{1}\fi + % + % Detect if the argument is a single token. If so, it might be a + % letter. Otherwise, the only valid thing it can be is a number. + % (We will always have one token, because of the test we just made. + % This is a good thing, since \splitoff doesn't work given nothing at + % all -- the first parameter is undelimited.) + \expandafter\splitoff\thearg\endmark + \ifx\rest\empty + % Only one token in the argument. It could still be anything. + % A ``lowercase letter'' is one whose \lccode is nonzero. + % An ``uppercase letter'' is one whose \lccode is both nonzero, and + % not equal to itself. + % Otherwise, we assume it's a number. + % + % We need the \relax at the end of the \ifnum lines to stop TeX from + % continuing to look for a <number>. + % + \ifnum\lccode\expandafter`\thearg=0\relax + \numericenumerate % a number (we hope) + \else + % It's a letter. + \ifnum\lccode\expandafter`\thearg=\expandafter`\thearg\relax + \lowercaseenumerate % lowercase letter + \else + \uppercaseenumerate % uppercase letter + \fi + \fi + \else + % Multiple tokens in the argument. We hope it's a number. + \numericenumerate + \fi +} + +% An @enumerate whose labels are integers. The starting integer is +% given in \thearg. +% +\def\numericenumerate{% + \itemno = \thearg + \startenumeration{\the\itemno}% +} + +% The starting (lowercase) letter is in \thearg. +\def\lowercaseenumerate{% + \itemno = \expandafter`\thearg + \startenumeration{% + % Be sure we're not beyond the end of the alphabet. + \ifnum\itemno=0 + \errmessage{No more lowercase letters in @enumerate; get a bigger + alphabet}% + \fi + \char\lccode\itemno + }% +} + +% The starting (uppercase) letter is in \thearg. +\def\uppercaseenumerate{% + \itemno = \expandafter`\thearg + \startenumeration{% + % Be sure we're not beyond the end of the alphabet. + \ifnum\itemno=0 + \errmessage{No more uppercase letters in @enumerate; get a bigger + alphabet} + \fi + \char\uccode\itemno + }% +} + +% Call \doitemize, adding a period to the first argument and supplying the +% common last two arguments. Also subtract one from the initial value in +% \itemno, since @item increments \itemno. +% +\def\startenumeration#1{% + \advance\itemno by -1 + \doitemize{#1.}\flushcr +} + +% @alphaenumerate and @capsenumerate are abbreviations for giving an arg +% to @enumerate. +% +\def\alphaenumerate{\enumerate{a}} +\def\capsenumerate{\enumerate{A}} +\def\Ealphaenumerate{\Eenumerate} +\def\Ecapsenumerate{\Eenumerate} + + +% @multitable macros +% Amy Hendrickson, 8/18/94, 3/6/96 +% +% @multitable ... @end multitable will make as many columns as desired. +% Contents of each column will wrap at width given in preamble. Width +% can be specified either with sample text given in a template line, +% or in percent of \hsize, the current width of text on page. + +% Table can continue over pages but will only break between lines. + +% To make preamble: +% +% Either define widths of columns in terms of percent of \hsize: +% @multitable @columnfractions .25 .3 .45 +% @item ... +% +% Numbers following @columnfractions are the percent of the total +% current hsize to be used for each column. You may use as many +% columns as desired. + + +% Or use a template: +% @multitable {Column 1 template} {Column 2 template} {Column 3 template} +% @item ... +% using the widest term desired in each column. + +% Each new table line starts with @item, each subsequent new column +% starts with @tab. Empty columns may be produced by supplying @tab's +% with nothing between them for as many times as empty columns are needed, +% ie, @tab@tab@tab will produce two empty columns. + +% @item, @tab do not need to be on their own lines, but it will not hurt +% if they are. + +% Sample multitable: + +% @multitable {Column 1 template} {Column 2 template} {Column 3 template} +% @item first col stuff @tab second col stuff @tab third col +% @item +% first col stuff +% @tab +% second col stuff +% @tab +% third col +% @item first col stuff @tab second col stuff +% @tab Many paragraphs of text may be used in any column. +% +% They will wrap at the width determined by the template. +% @item@tab@tab This will be in third column. +% @end multitable + +% Default dimensions may be reset by user. +% @multitableparskip is vertical space between paragraphs in table. +% @multitableparindent is paragraph indent in table. +% @multitablecolmargin is horizontal space to be left between columns. +% @multitablelinespace is space to leave between table items, baseline +% to baseline. +% 0pt means it depends on current normal line spacing. +% +\newskip\multitableparskip +\newskip\multitableparindent +\newdimen\multitablecolspace +\newskip\multitablelinespace +\multitableparskip=0pt +\multitableparindent=6pt +\multitablecolspace=12pt +\multitablelinespace=0pt + +% Macros used to set up halign preamble: +% +\let\endsetuptable\relax +\def\xendsetuptable{\endsetuptable} +\let\columnfractions\relax +\def\xcolumnfractions{\columnfractions} +\newif\ifsetpercent + +% #1 is the @columnfraction, usually a decimal number like .5, but might +% be just 1. We just use it, whatever it is. +% +\def\pickupwholefraction#1 {% + \global\advance\colcount by 1 + \expandafter\xdef\csname col\the\colcount\endcsname{#1\hsize}% + \setuptable +} + +\newcount\colcount +\def\setuptable#1{% + \def\firstarg{#1}% + \ifx\firstarg\xendsetuptable + \let\go = \relax + \else + \ifx\firstarg\xcolumnfractions + \global\setpercenttrue + \else + \ifsetpercent + \let\go\pickupwholefraction + \else + \global\advance\colcount by 1 + \setbox0=\hbox{#1\unskip\space}% Add a normal word space as a + % separator; typically that is always in the input, anyway. + \expandafter\xdef\csname col\the\colcount\endcsname{\the\wd0}% + \fi + \fi + \ifx\go\pickupwholefraction + % Put the argument back for the \pickupwholefraction call, so + % we'll always have a period there to be parsed. + \def\go{\pickupwholefraction#1}% + \else + \let\go = \setuptable + \fi% + \fi + \go +} + +% multitable-only commands. +% +% @headitem starts a heading row, which we typeset in bold. +% Assignments have to be global since we are inside the implicit group +% of an alignment entry. Note that \everycr resets \everytab. +\def\headitem{\checkenv\multitable \crcr \global\everytab={\bf}\the\everytab}% +% +% A \tab used to include \hskip1sp. But then the space in a template +% line is not enough. That is bad. So let's go back to just `&' until +% we encounter the problem it was intended to solve again. +% --karl, nathan@acm.org, 20apr99. +\def\tab{\checkenv\multitable &\the\everytab}% + +% @multitable ... @end multitable definitions: +% +\newtoks\everytab % insert after every tab. +% +\envdef\multitable{% + \vskip\parskip + \startsavinginserts + % + % @item within a multitable starts a normal row. + % We use \def instead of \let so that if one of the multitable entries + % contains an @itemize, we don't choke on the \item (seen as \crcr aka + % \endtemplate) expanding \doitemize. + \def\item{\crcr}% + % + \tolerance=9500 + \hbadness=9500 + \setmultitablespacing + \parskip=\multitableparskip + \parindent=\multitableparindent + \overfullrule=0pt + \global\colcount=0 + % + \everycr = {% + \noalign{% + \global\everytab={}% + \global\colcount=0 % Reset the column counter. + % Check for saved footnotes, etc. + \checkinserts + % Keeps underfull box messages off when table breaks over pages. + %\filbreak + % Maybe so, but it also creates really weird page breaks when the + % table breaks over pages. Wouldn't \vfil be better? Wait until the + % problem manifests itself, so it can be fixed for real --karl. + }% + }% + % + \parsearg\domultitable +} +\def\domultitable#1{% + % To parse everything between @multitable and @item: + \setuptable#1 \endsetuptable + % + % This preamble sets up a generic column definition, which will + % be used as many times as user calls for columns. + % \vtop will set a single line and will also let text wrap and + % continue for many paragraphs if desired. + \halign\bgroup &% + \global\advance\colcount by 1 + \multistrut + \vtop{% + % Use the current \colcount to find the correct column width: + \hsize=\expandafter\csname col\the\colcount\endcsname + % + % In order to keep entries from bumping into each other + % we will add a \leftskip of \multitablecolspace to all columns after + % the first one. + % + % If a template has been used, we will add \multitablecolspace + % to the width of each template entry. + % + % If the user has set preamble in terms of percent of \hsize we will + % use that dimension as the width of the column, and the \leftskip + % will keep entries from bumping into each other. Table will start at + % left margin and final column will justify at right margin. + % + % Make sure we don't inherit \rightskip from the outer environment. + \rightskip=0pt + \ifnum\colcount=1 + % The first column will be indented with the surrounding text. + \advance\hsize by\leftskip + \else + \ifsetpercent \else + % If user has not set preamble in terms of percent of \hsize + % we will advance \hsize by \multitablecolspace. + \advance\hsize by \multitablecolspace + \fi + % In either case we will make \leftskip=\multitablecolspace: + \leftskip=\multitablecolspace + \fi + % Ignoring space at the beginning and end avoids an occasional spurious + % blank line, when TeX decides to break the line at the space before the + % box from the multistrut, so the strut ends up on a line by itself. + % For example: + % @multitable @columnfractions .11 .89 + % @item @code{#} + % @tab Legal holiday which is valid in major parts of the whole country. + % Is automatically provided with highlighting sequences respectively + % marking characters. + \noindent\ignorespaces##\unskip\multistrut + }\cr +} +\def\Emultitable{% + \crcr + \egroup % end the \halign + \global\setpercentfalse +} + +\def\setmultitablespacing{% + \def\multistrut{\strut}% just use the standard line spacing + % + % Compute \multitablelinespace (if not defined by user) for use in + % \multitableparskip calculation. We used define \multistrut based on + % this, but (ironically) that caused the spacing to be off. + % See bug-texinfo report from Werner Lemberg, 31 Oct 2004 12:52:20 +0100. +\ifdim\multitablelinespace=0pt +\setbox0=\vbox{X}\global\multitablelinespace=\the\baselineskip +\global\advance\multitablelinespace by-\ht0 +\fi +%% Test to see if parskip is larger than space between lines of +%% table. If not, do nothing. +%% If so, set to same dimension as multitablelinespace. +\ifdim\multitableparskip>\multitablelinespace +\global\multitableparskip=\multitablelinespace +\global\advance\multitableparskip-7pt %% to keep parskip somewhat smaller + %% than skip between lines in the table. +\fi% +\ifdim\multitableparskip=0pt +\global\multitableparskip=\multitablelinespace +\global\advance\multitableparskip-7pt %% to keep parskip somewhat smaller + %% than skip between lines in the table. +\fi} + + +\message{conditionals,} + +% @iftex, @ifnotdocbook, @ifnothtml, @ifnotinfo, @ifnotplaintext, +% @ifnotxml always succeed. They currently do nothing; we don't +% attempt to check whether the conditionals are properly nested. But we +% have to remember that they are conditionals, so that @end doesn't +% attempt to close an environment group. +% +\def\makecond#1{% + \expandafter\let\csname #1\endcsname = \relax + \expandafter\let\csname iscond.#1\endcsname = 1 +} +\makecond{iftex} +\makecond{ifnotdocbook} +\makecond{ifnothtml} +\makecond{ifnotinfo} +\makecond{ifnotplaintext} +\makecond{ifnotxml} + +% Ignore @ignore, @ifhtml, @ifinfo, and the like. +% +\def\direntry{\doignore{direntry}} +\def\documentdescription{\doignore{documentdescription}} +\def\docbook{\doignore{docbook}} +\def\html{\doignore{html}} +\def\ifdocbook{\doignore{ifdocbook}} +\def\ifhtml{\doignore{ifhtml}} +\def\ifinfo{\doignore{ifinfo}} +\def\ifnottex{\doignore{ifnottex}} +\def\ifplaintext{\doignore{ifplaintext}} +\def\ifxml{\doignore{ifxml}} +\def\ignore{\doignore{ignore}} +\def\menu{\doignore{menu}} +\def\xml{\doignore{xml}} + +% Ignore text until a line `@end #1', keeping track of nested conditionals. +% +% A count to remember the depth of nesting. +\newcount\doignorecount + +\def\doignore#1{\begingroup + % Scan in ``verbatim'' mode: + \obeylines + \catcode`\@ = \other + \catcode`\{ = \other + \catcode`\} = \other + % + % Make sure that spaces turn into tokens that match what \doignoretext wants. + \spaceisspace + % + % Count number of #1's that we've seen. + \doignorecount = 0 + % + % Swallow text until we reach the matching `@end #1'. + \dodoignore{#1}% +} + +{ \catcode`_=11 % We want to use \_STOP_ which cannot appear in texinfo source. + \obeylines % + % + \gdef\dodoignore#1{% + % #1 contains the command name as a string, e.g., `ifinfo'. + % + % Define a command to find the next `@end #1'. + \long\def\doignoretext##1^^M@end #1{% + \doignoretextyyy##1^^M@#1\_STOP_}% + % + % And this command to find another #1 command, at the beginning of a + % line. (Otherwise, we would consider a line `@c @ifset', for + % example, to count as an @ifset for nesting.) + \long\def\doignoretextyyy##1^^M@#1##2\_STOP_{\doignoreyyy{##2}\_STOP_}% + % + % And now expand that command. + \doignoretext ^^M% + }% +} + +\def\doignoreyyy#1{% + \def\temp{#1}% + \ifx\temp\empty % Nothing found. + \let\next\doignoretextzzz + \else % Found a nested condition, ... + \advance\doignorecount by 1 + \let\next\doignoretextyyy % ..., look for another. + % If we're here, #1 ends with ^^M\ifinfo (for example). + \fi + \next #1% the token \_STOP_ is present just after this macro. +} + +% We have to swallow the remaining "\_STOP_". +% +\def\doignoretextzzz#1{% + \ifnum\doignorecount = 0 % We have just found the outermost @end. + \let\next\enddoignore + \else % Still inside a nested condition. + \advance\doignorecount by -1 + \let\next\doignoretext % Look for the next @end. + \fi + \next +} + +% Finish off ignored text. +{ \obeylines% + % Ignore anything after the last `@end #1'; this matters in verbatim + % environments, where otherwise the newline after an ignored conditional + % would result in a blank line in the output. + \gdef\enddoignore#1^^M{\endgroup\ignorespaces}% +} + + +% @set VAR sets the variable VAR to an empty value. +% @set VAR REST-OF-LINE sets VAR to the value REST-OF-LINE. +% +% Since we want to separate VAR from REST-OF-LINE (which might be +% empty), we can't just use \parsearg; we have to insert a space of our +% own to delimit the rest of the line, and then take it out again if we +% didn't need it. +% We rely on the fact that \parsearg sets \catcode`\ =10. +% +\parseargdef\set{\setyyy#1 \endsetyyy} +\def\setyyy#1 #2\endsetyyy{% + {% + \makevalueexpandable + \def\temp{#2}% + \edef\next{\gdef\makecsname{SET#1}}% + \ifx\temp\empty + \next{}% + \else + \setzzz#2\endsetzzz + \fi + }% +} +% Remove the trailing space \setxxx inserted. +\def\setzzz#1 \endsetzzz{\next{#1}} + +% @clear VAR clears (i.e., unsets) the variable VAR. +% +\parseargdef\clear{% + {% + \makevalueexpandable + \global\expandafter\let\csname SET#1\endcsname=\relax + }% +} + +% @value{foo} gets the text saved in variable foo. +\def\value{\begingroup\makevalueexpandable\valuexxx} +\def\valuexxx#1{\expandablevalue{#1}\endgroup} +{ + \catcode`\- = \active \catcode`\_ = \active + % + \gdef\makevalueexpandable{% + \let\value = \expandablevalue + % We don't want these characters active, ... + \catcode`\-=\other \catcode`\_=\other + % ..., but we might end up with active ones in the argument if + % we're called from @code, as @code{@value{foo-bar_}}, though. + % So \let them to their normal equivalents. + \let-\realdash \let_\normalunderscore + } +} + +% We have this subroutine so that we can handle at least some @value's +% properly in indexes (we call \makevalueexpandable in \indexdummies). +% The command has to be fully expandable (if the variable is set), since +% the result winds up in the index file. This means that if the +% variable's value contains other Texinfo commands, it's almost certain +% it will fail (although perhaps we could fix that with sufficient work +% to do a one-level expansion on the result, instead of complete). +% +\def\expandablevalue#1{% + \expandafter\ifx\csname SET#1\endcsname\relax + {[No value for ``#1'']}% + \message{Variable `#1', used in @value, is not set.}% + \else + \csname SET#1\endcsname + \fi +} + +% @ifset VAR ... @end ifset reads the `...' iff VAR has been defined +% with @set. +% +% To get special treatment of `@end ifset,' call \makeond and the redefine. +% +\makecond{ifset} +\def\ifset{\parsearg{\doifset{\let\next=\ifsetfail}}} +\def\doifset#1#2{% + {% + \makevalueexpandable + \let\next=\empty + \expandafter\ifx\csname SET#2\endcsname\relax + #1% If not set, redefine \next. + \fi + \expandafter + }\next +} +\def\ifsetfail{\doignore{ifset}} + +% @ifclear VAR ... @end ifclear reads the `...' iff VAR has never been +% defined with @set, or has been undefined with @clear. +% +% The `\else' inside the `\doifset' parameter is a trick to reuse the +% above code: if the variable is not set, do nothing, if it is set, +% then redefine \next to \ifclearfail. +% +\makecond{ifclear} +\def\ifclear{\parsearg{\doifset{\else \let\next=\ifclearfail}}} +\def\ifclearfail{\doignore{ifclear}} + +% @dircategory CATEGORY -- specify a category of the dir file +% which this file should belong to. Ignore this in TeX. +\let\dircategory=\comment + +% @defininfoenclose. +\let\definfoenclose=\comment + + +\message{indexing,} +% Index generation facilities + +% Define \newwrite to be identical to plain tex's \newwrite +% except not \outer, so it can be used within macros and \if's. +\edef\newwrite{\makecsname{ptexnewwrite}} + +% \newindex {foo} defines an index named foo. +% It automatically defines \fooindex such that +% \fooindex ...rest of line... puts an entry in the index foo. +% It also defines \fooindfile to be the number of the output channel for +% the file that accumulates this index. The file's extension is foo. +% The name of an index should be no more than 2 characters long +% for the sake of vms. +% +\def\newindex#1{% + \iflinks + \expandafter\newwrite \csname#1indfile\endcsname + \openout \csname#1indfile\endcsname \jobname.#1 % Open the file + \fi + \expandafter\xdef\csname#1index\endcsname{% % Define @#1index + \noexpand\doindex{#1}} +} + +% @defindex foo == \newindex{foo} +% +\def\defindex{\parsearg\newindex} + +% Define @defcodeindex, like @defindex except put all entries in @code. +% +\def\defcodeindex{\parsearg\newcodeindex} +% +\def\newcodeindex#1{% + \iflinks + \expandafter\newwrite \csname#1indfile\endcsname + \openout \csname#1indfile\endcsname \jobname.#1 + \fi + \expandafter\xdef\csname#1index\endcsname{% + \noexpand\docodeindex{#1}}% +} + + +% @synindex foo bar makes index foo feed into index bar. +% Do this instead of @defindex foo if you don't want it as a separate index. +% +% @syncodeindex foo bar similar, but put all entries made for index foo +% inside @code. +% +\def\synindex#1 #2 {\dosynindex\doindex{#1}{#2}} +\def\syncodeindex#1 #2 {\dosynindex\docodeindex{#1}{#2}} + +% #1 is \doindex or \docodeindex, #2 the index getting redefined (foo), +% #3 the target index (bar). +\def\dosynindex#1#2#3{% + % Only do \closeout if we haven't already done it, else we'll end up + % closing the target index. + \expandafter \ifx\csname donesynindex#2\endcsname \undefined + % The \closeout helps reduce unnecessary open files; the limit on the + % Acorn RISC OS is a mere 16 files. + \expandafter\closeout\csname#2indfile\endcsname + \expandafter\let\csname\donesynindex#2\endcsname = 1 + \fi + % redefine \fooindfile: + \expandafter\let\expandafter\temp\expandafter=\csname#3indfile\endcsname + \expandafter\let\csname#2indfile\endcsname=\temp + % redefine \fooindex: + \expandafter\xdef\csname#2index\endcsname{\noexpand#1{#3}}% +} + +% Define \doindex, the driver for all \fooindex macros. +% Argument #1 is generated by the calling \fooindex macro, +% and it is "foo", the name of the index. + +% \doindex just uses \parsearg; it calls \doind for the actual work. +% This is because \doind is more useful to call from other macros. + +% There is also \dosubind {index}{topic}{subtopic} +% which makes an entry in a two-level index such as the operation index. + +\def\doindex#1{\edef\indexname{#1}\parsearg\singleindexer} +\def\singleindexer #1{\doind{\indexname}{#1}} + +% like the previous two, but they put @code around the argument. +\def\docodeindex#1{\edef\indexname{#1}\parsearg\singlecodeindexer} +\def\singlecodeindexer #1{\doind{\indexname}{\code{#1}}} + +% Take care of Texinfo commands that can appear in an index entry. +% Since there are some commands we want to expand, and others we don't, +% we have to laboriously prevent expansion for those that we don't. +% +\def\indexdummies{% + \escapechar = `\\ % use backslash in output files. + \def\@{@}% change to @@ when we switch to @ as escape char in index files. + \def\ {\realbackslash\space }% + % + % Need these in case \tex is in effect and \{ is a \delimiter again. + % But can't use \lbracecmd and \rbracecmd because texindex assumes + % braces and backslashes are used only as delimiters. + \let\{ = \mylbrace + \let\} = \myrbrace + % + % I don't entirely understand this, but when an index entry is + % generated from a macro call, the \endinput which \scanmacro inserts + % causes processing to be prematurely terminated. This is, + % apparently, because \indexsorttmp is fully expanded, and \endinput + % is an expandable command. The redefinition below makes \endinput + % disappear altogether for that purpose -- although logging shows that + % processing continues to some further point. On the other hand, it + % seems \endinput does not hurt in the printed index arg, since that + % is still getting written without apparent harm. + % + % Sample source (mac-idx3.tex, reported by Graham Percival to + % help-texinfo, 22may06): + % @macro funindex {WORD} + % @findex xyz + % @end macro + % ... + % @funindex commtest + % + % The above is not enough to reproduce the bug, but it gives the flavor. + % + % Sample whatsit resulting: + % .@write3{\entry{xyz}{@folio }{@code {xyz@endinput }}} + % + % So: + \let\endinput = \empty + % + % Do the redefinitions. + \commondummies +} + +% For the aux and toc files, @ is the escape character. So we want to +% redefine everything using @ as the escape character (instead of +% \realbackslash, still used for index files). When everything uses @, +% this will be simpler. +% +\def\atdummies{% + \def\@{@@}% + \def\ {@ }% + \let\{ = \lbraceatcmd + \let\} = \rbraceatcmd + % + % Do the redefinitions. + \commondummies + \otherbackslash +} + +% Called from \indexdummies and \atdummies. +% +\def\commondummies{% + % + % \definedummyword defines \#1 as \string\#1\space, thus effectively + % preventing its expansion. This is used only for control% words, + % not control letters, because the \space would be incorrect for + % control characters, but is needed to separate the control word + % from whatever follows. + % + % For control letters, we have \definedummyletter, which omits the + % space. + % + % These can be used both for control words that take an argument and + % those that do not. If it is followed by {arg} in the input, then + % that will dutifully get written to the index (or wherever). + % + \def\definedummyword ##1{\def##1{\string##1\space}}% + \def\definedummyletter##1{\def##1{\string##1}}% + \let\definedummyaccent\definedummyletter + % + \commondummiesnofonts + % + \definedummyletter\_% + % + % Non-English letters. + \definedummyword\AA + \definedummyword\AE + \definedummyword\L + \definedummyword\OE + \definedummyword\O + \definedummyword\aa + \definedummyword\ae + \definedummyword\l + \definedummyword\oe + \definedummyword\o + \definedummyword\ss + \definedummyword\exclamdown + \definedummyword\questiondown + \definedummyword\ordf + \definedummyword\ordm + % + % Although these internal commands shouldn't show up, sometimes they do. + \definedummyword\bf + \definedummyword\gtr + \definedummyword\hat + \definedummyword\less + \definedummyword\sf + \definedummyword\sl + \definedummyword\tclose + \definedummyword\tt + % + \definedummyword\LaTeX + \definedummyword\TeX + % + % Assorted special characters. + \definedummyword\bullet + \definedummyword\comma + \definedummyword\copyright + \definedummyword\registeredsymbol + \definedummyword\dots + \definedummyword\enddots + \definedummyword\equiv + \definedummyword\error + \definedummyword\euro + \definedummyword\guillemetleft + \definedummyword\guillemetright + \definedummyword\guilsinglleft + \definedummyword\guilsinglright + \definedummyword\expansion + \definedummyword\minus + \definedummyword\pounds + \definedummyword\point + \definedummyword\print + \definedummyword\quotedblbase + \definedummyword\quotedblleft + \definedummyword\quotedblright + \definedummyword\quoteleft + \definedummyword\quoteright + \definedummyword\quotesinglbase + \definedummyword\result + \definedummyword\textdegree + % + % We want to disable all macros so that they are not expanded by \write. + \macrolist + % + \normalturnoffactive + % + % Handle some cases of @value -- where it does not contain any + % (non-fully-expandable) commands. + \makevalueexpandable +} + +% \commondummiesnofonts: common to \commondummies and \indexnofonts. +% +\def\commondummiesnofonts{% + % Control letters and accents. + \definedummyletter\!% + \definedummyaccent\"% + \definedummyaccent\'% + \definedummyletter\*% + \definedummyaccent\,% + \definedummyletter\.% + \definedummyletter\/% + \definedummyletter\:% + \definedummyaccent\=% + \definedummyletter\?% + \definedummyaccent\^% + \definedummyaccent\`% + \definedummyaccent\~% + \definedummyword\u + \definedummyword\v + \definedummyword\H + \definedummyword\dotaccent + \definedummyword\ringaccent + \definedummyword\tieaccent + \definedummyword\ubaraccent + \definedummyword\udotaccent + \definedummyword\dotless + % + % Texinfo font commands. + \definedummyword\b + \definedummyword\i + \definedummyword\r + \definedummyword\sc + \definedummyword\t + % + % Commands that take arguments. + \definedummyword\acronym + \definedummyword\cite + \definedummyword\code + \definedummyword\command + \definedummyword\dfn + \definedummyword\emph + \definedummyword\env + \definedummyword\file + \definedummyword\kbd + \definedummyword\key + \definedummyword\math + \definedummyword\option + \definedummyword\pxref + \definedummyword\ref + \definedummyword\samp + \definedummyword\strong + \definedummyword\tie + \definedummyword\uref + \definedummyword\url + \definedummyword\var + \definedummyword\verb + \definedummyword\w + \definedummyword\xref +} + +% \indexnofonts is used when outputting the strings to sort the index +% by, and when constructing control sequence names. It eliminates all +% control sequences and just writes whatever the best ASCII sort string +% would be for a given command (usually its argument). +% +\def\indexnofonts{% + % Accent commands should become @asis. + \def\definedummyaccent##1{\let##1\asis}% + % We can just ignore other control letters. + \def\definedummyletter##1{\let##1\empty}% + % Hopefully, all control words can become @asis. + \let\definedummyword\definedummyaccent + % + \commondummiesnofonts + % + % Don't no-op \tt, since it isn't a user-level command + % and is used in the definitions of the active chars like <, >, |, etc. + % Likewise with the other plain tex font commands. + %\let\tt=\asis + % + \def\ { }% + \def\@{@}% + % how to handle braces? + \def\_{\normalunderscore}% + % + % Non-English letters. + \def\AA{AA}% + \def\AE{AE}% + \def\L{L}% + \def\OE{OE}% + \def\O{O}% + \def\aa{aa}% + \def\ae{ae}% + \def\l{l}% + \def\oe{oe}% + \def\o{o}% + \def\ss{ss}% + \def\exclamdown{!}% + \def\questiondown{?}% + \def\ordf{a}% + \def\ordm{o}% + % + \def\LaTeX{LaTeX}% + \def\TeX{TeX}% + % + % Assorted special characters. + % (The following {} will end up in the sort string, but that's ok.) + \def\bullet{bullet}% + \def\comma{,}% + \def\copyright{copyright}% + \def\registeredsymbol{R}% + \def\dots{...}% + \def\enddots{...}% + \def\equiv{==}% + \def\error{error}% + \def\euro{euro}% + \def\guillemetleft{<<}% + \def\guillemetright{>>}% + \def\guilsinglleft{<}% + \def\guilsinglright{>}% + \def\expansion{==>}% + \def\minus{-}% + \def\pounds{pounds}% + \def\point{.}% + \def\print{-|}% + \def\quotedblbase{"}% + \def\quotedblleft{"}% + \def\quotedblright{"}% + \def\quoteleft{`}% + \def\quoteright{'}% + \def\quotesinglbase{,}% + \def\result{=>}% + \def\textdegree{degrees}% + % + % We need to get rid of all macros, leaving only the arguments (if present). + % Of course this is not nearly correct, but it is the best we can do for now. + % makeinfo does not expand macros in the argument to @deffn, which ends up + % writing an index entry, and texindex isn't prepared for an index sort entry + % that starts with \. + % + % Since macro invocations are followed by braces, we can just redefine them + % to take a single TeX argument. The case of a macro invocation that + % goes to end-of-line is not handled. + % + \macrolist +} + +\let\indexbackslash=0 %overridden during \printindex. +\let\SETmarginindex=\relax % put index entries in margin (undocumented)? + +% Most index entries go through here, but \dosubind is the general case. +% #1 is the index name, #2 is the entry text. +\def\doind#1#2{\dosubind{#1}{#2}{}} + +% Workhorse for all \fooindexes. +% #1 is name of index, #2 is stuff to put there, #3 is subentry -- +% empty if called from \doind, as we usually are (the main exception +% is with most defuns, which call us directly). +% +\def\dosubind#1#2#3{% + \iflinks + {% + % Store the main index entry text (including the third arg). + \toks0 = {#2}% + % If third arg is present, precede it with a space. + \def\thirdarg{#3}% + \ifx\thirdarg\empty \else + \toks0 = \expandafter{\the\toks0 \space #3}% + \fi + % + \edef\writeto{\csname#1indfile\endcsname}% + % + \safewhatsit\dosubindwrite + }% + \fi +} + +% Write the entry in \toks0 to the index file: +% +\def\dosubindwrite{% + % Put the index entry in the margin if desired. + \ifx\SETmarginindex\relax\else + \insert\margin{\hbox{\vrule height8pt depth3pt width0pt \the\toks0}}% + \fi + % + % Remember, we are within a group. + \indexdummies % Must do this here, since \bf, etc expand at this stage + \def\backslashcurfont{\indexbackslash}% \indexbackslash isn't defined now + % so it will be output as is; and it will print as backslash. + % + % Process the index entry with all font commands turned off, to + % get the string to sort by. + {\indexnofonts + \edef\temp{\the\toks0}% need full expansion + \xdef\indexsorttmp{\temp}% + }% + % + % Set up the complete index entry, with both the sort key and + % the original text, including any font commands. We write + % three arguments to \entry to the .?? file (four in the + % subentry case), texindex reduces to two when writing the .??s + % sorted result. + \edef\temp{% + \write\writeto{% + \string\entry{\indexsorttmp}{\noexpand\folio}{\the\toks0}}% + }% + \temp +} + +% Take care of unwanted page breaks/skips around a whatsit: +% +% If a skip is the last thing on the list now, preserve it +% by backing up by \lastskip, doing the \write, then inserting +% the skip again. Otherwise, the whatsit generated by the +% \write or \pdfdest will make \lastskip zero. The result is that +% sequences like this: +% @end defun +% @tindex whatever +% @defun ... +% will have extra space inserted, because the \medbreak in the +% start of the @defun won't see the skip inserted by the @end of +% the previous defun. +% +% But don't do any of this if we're not in vertical mode. We +% don't want to do a \vskip and prematurely end a paragraph. +% +% Avoid page breaks due to these extra skips, too. +% +% But wait, there is a catch there: +% We'll have to check whether \lastskip is zero skip. \ifdim is not +% sufficient for this purpose, as it ignores stretch and shrink parts +% of the skip. The only way seems to be to check the textual +% representation of the skip. +% +% The following is almost like \def\zeroskipmacro{0.0pt} except that +% the ``p'' and ``t'' characters have catcode \other, not 11 (letter). +% +\edef\zeroskipmacro{\expandafter\the\csname z@skip\endcsname} +% +\newskip\whatsitskip +\newcount\whatsitpenalty +% +% ..., ready, GO: +% +\def\safewhatsit#1{% +\ifhmode + #1% +\else + % \lastskip and \lastpenalty cannot both be nonzero simultaneously. + \whatsitskip = \lastskip + \edef\lastskipmacro{\the\lastskip}% + \whatsitpenalty = \lastpenalty + % + % If \lastskip is nonzero, that means the last item was a + % skip. And since a skip is discardable, that means this + % -\whatsitskip glue we're inserting is preceded by a + % non-discardable item, therefore it is not a potential + % breakpoint, therefore no \nobreak needed. + \ifx\lastskipmacro\zeroskipmacro + \else + \vskip-\whatsitskip + \fi + % + #1% + % + \ifx\lastskipmacro\zeroskipmacro + % If \lastskip was zero, perhaps the last item was a penalty, and + % perhaps it was >=10000, e.g., a \nobreak. In that case, we want + % to re-insert the same penalty (values >10000 are used for various + % signals); since we just inserted a non-discardable item, any + % following glue (such as a \parskip) would be a breakpoint. For example: + % + % @deffn deffn-whatever + % @vindex index-whatever + % Description. + % would allow a break between the index-whatever whatsit + % and the "Description." paragraph. + \ifnum\whatsitpenalty>9999 \penalty\whatsitpenalty \fi + \else + % On the other hand, if we had a nonzero \lastskip, + % this make-up glue would be preceded by a non-discardable item + % (the whatsit from the \write), so we must insert a \nobreak. + \nobreak\vskip\whatsitskip + \fi +\fi +} + +% The index entry written in the file actually looks like +% \entry {sortstring}{page}{topic} +% or +% \entry {sortstring}{page}{topic}{subtopic} +% The texindex program reads in these files and writes files +% containing these kinds of lines: +% \initial {c} +% before the first topic whose initial is c +% \entry {topic}{pagelist} +% for a topic that is used without subtopics +% \primary {topic} +% for the beginning of a topic that is used with subtopics +% \secondary {subtopic}{pagelist} +% for each subtopic. + +% Define the user-accessible indexing commands +% @findex, @vindex, @kindex, @cindex. + +\def\findex {\fnindex} +\def\kindex {\kyindex} +\def\cindex {\cpindex} +\def\vindex {\vrindex} +\def\tindex {\tpindex} +\def\pindex {\pgindex} + +\def\cindexsub {\begingroup\obeylines\cindexsub} +{\obeylines % +\gdef\cindexsub "#1" #2^^M{\endgroup % +\dosubind{cp}{#2}{#1}}} + +% Define the macros used in formatting output of the sorted index material. + +% @printindex causes a particular index (the ??s file) to get printed. +% It does not print any chapter heading (usually an @unnumbered). +% +\parseargdef\printindex{\begingroup + \dobreak \chapheadingskip{10000}% + % + \smallfonts \rm + \tolerance = 9500 + \plainfrenchspacing + \everypar = {}% don't want the \kern\-parindent from indentation suppression. + % + % See if the index file exists and is nonempty. + % Change catcode of @ here so that if the index file contains + % \initial {@} + % as its first line, TeX doesn't complain about mismatched braces + % (because it thinks @} is a control sequence). + \catcode`\@ = 11 + \openin 1 \jobname.#1s + \ifeof 1 + % \enddoublecolumns gets confused if there is no text in the index, + % and it loses the chapter title and the aux file entries for the + % index. The easiest way to prevent this problem is to make sure + % there is some text. + \putwordIndexNonexistent + \else + % + % If the index file exists but is empty, then \openin leaves \ifeof + % false. We have to make TeX try to read something from the file, so + % it can discover if there is anything in it. + \read 1 to \temp + \ifeof 1 + \putwordIndexIsEmpty + \else + % Index files are almost Texinfo source, but we use \ as the escape + % character. It would be better to use @, but that's too big a change + % to make right now. + \def\indexbackslash{\backslashcurfont}% + \catcode`\\ = 0 + \escapechar = `\\ + \begindoublecolumns + \input \jobname.#1s + \enddoublecolumns + \fi + \fi + \closein 1 +\endgroup} + +% These macros are used by the sorted index file itself. +% Change them to control the appearance of the index. + +\def\initial#1{{% + % Some minor font changes for the special characters. + \let\tentt=\sectt \let\tt=\sectt \let\sf=\sectt + % + % Remove any glue we may have, we'll be inserting our own. + \removelastskip + % + % We like breaks before the index initials, so insert a bonus. + \nobreak + \vskip 0pt plus 3\baselineskip + \penalty 0 + \vskip 0pt plus -3\baselineskip + % + % Typeset the initial. Making this add up to a whole number of + % baselineskips increases the chance of the dots lining up from column + % to column. It still won't often be perfect, because of the stretch + % we need before each entry, but it's better. + % + % No shrink because it confuses \balancecolumns. + \vskip 1.67\baselineskip plus .5\baselineskip + \leftline{\secbf #1}% + % Do our best not to break after the initial. + \nobreak + \vskip .33\baselineskip plus .1\baselineskip +}} + +% \entry typesets a paragraph consisting of the text (#1), dot leaders, and +% then page number (#2) flushed to the right margin. It is used for index +% and table of contents entries. The paragraph is indented by \leftskip. +% +% A straightforward implementation would start like this: +% \def\entry#1#2{... +% But this frozes the catcodes in the argument, and can cause problems to +% @code, which sets - active. This problem was fixed by a kludge--- +% ``-'' was active throughout whole index, but this isn't really right. +% +% The right solution is to prevent \entry from swallowing the whole text. +% --kasal, 21nov03 +\def\entry{% + \begingroup + % + % Start a new paragraph if necessary, so our assignments below can't + % affect previous text. + \par + % + % Do not fill out the last line with white space. + \parfillskip = 0in + % + % No extra space above this paragraph. + \parskip = 0in + % + % Do not prefer a separate line ending with a hyphen to fewer lines. + \finalhyphendemerits = 0 + % + % \hangindent is only relevant when the entry text and page number + % don't both fit on one line. In that case, bob suggests starting the + % dots pretty far over on the line. Unfortunately, a large + % indentation looks wrong when the entry text itself is broken across + % lines. So we use a small indentation and put up with long leaders. + % + % \hangafter is reset to 1 (which is the value we want) at the start + % of each paragraph, so we need not do anything with that. + \hangindent = 2em + % + % When the entry text needs to be broken, just fill out the first line + % with blank space. + \rightskip = 0pt plus1fil + % + % A bit of stretch before each entry for the benefit of balancing + % columns. + \vskip 0pt plus1pt + % + % Swallow the left brace of the text (first parameter): + \afterassignment\doentry + \let\temp = +} +\def\doentry{% + \bgroup % Instead of the swallowed brace. + \noindent + \aftergroup\finishentry + % And now comes the text of the entry. +} +\def\finishentry#1{% + % #1 is the page number. + % + % The following is kludged to not output a line of dots in the index if + % there are no page numbers. The next person who breaks this will be + % cursed by a Unix daemon. + \setbox\boxA = \hbox{#1}% + \ifdim\wd\boxA = 0pt + \ % + \else + % + % If we must, put the page number on a line of its own, and fill out + % this line with blank space. (The \hfil is overwhelmed with the + % fill leaders glue in \indexdotfill if the page number does fit.) + \hfil\penalty50 + \null\nobreak\indexdotfill % Have leaders before the page number. + % + % The `\ ' here is removed by the implicit \unskip that TeX does as + % part of (the primitive) \par. Without it, a spurious underfull + % \hbox ensues. + \ifpdf + \pdfgettoks#1.% + \ \the\toksA + \else + \ #1% + \fi + \fi + \par + \endgroup +} + +% Like plain.tex's \dotfill, except uses up at least 1 em. +\def\indexdotfill{\cleaders + \hbox{$\mathsurround=0pt \mkern1.5mu.\mkern1.5mu$}\hskip 1em plus 1fill} + +\def\primary #1{\line{#1\hfil}} + +\newskip\secondaryindent \secondaryindent=0.5cm +\def\secondary#1#2{{% + \parfillskip=0in + \parskip=0in + \hangindent=1in + \hangafter=1 + \noindent\hskip\secondaryindent\hbox{#1}\indexdotfill + \ifpdf + \pdfgettoks#2.\ \the\toksA % The page number ends the paragraph. + \else + #2 + \fi + \par +}} + +% Define two-column mode, which we use to typeset indexes. +% Adapted from the TeXbook, page 416, which is to say, +% the manmac.tex format used to print the TeXbook itself. +\catcode`\@=11 + +\newbox\partialpage +\newdimen\doublecolumnhsize + +\def\begindoublecolumns{\begingroup % ended by \enddoublecolumns + % Grab any single-column material above us. + \output = {% + % + % Here is a possibility not foreseen in manmac: if we accumulate a + % whole lot of material, we might end up calling this \output + % routine twice in a row (see the doublecol-lose test, which is + % essentially a couple of indexes with @setchapternewpage off). In + % that case we just ship out what is in \partialpage with the normal + % output routine. Generally, \partialpage will be empty when this + % runs and this will be a no-op. See the indexspread.tex test case. + \ifvoid\partialpage \else + \onepageout{\pagecontents\partialpage}% + \fi + % + \global\setbox\partialpage = \vbox{% + % Unvbox the main output page. + \unvbox\PAGE + \kern-\topskip \kern\baselineskip + }% + }% + \eject % run that output routine to set \partialpage + % + % Use the double-column output routine for subsequent pages. + \output = {\doublecolumnout}% + % + % Change the page size parameters. We could do this once outside this + % routine, in each of @smallbook, @afourpaper, and the default 8.5x11 + % format, but then we repeat the same computation. Repeating a couple + % of assignments once per index is clearly meaningless for the + % execution time, so we may as well do it in one place. + % + % First we halve the line length, less a little for the gutter between + % the columns. We compute the gutter based on the line length, so it + % changes automatically with the paper format. The magic constant + % below is chosen so that the gutter has the same value (well, +-<1pt) + % as it did when we hard-coded it. + % + % We put the result in a separate register, \doublecolumhsize, so we + % can restore it in \pagesofar, after \hsize itself has (potentially) + % been clobbered. + % + \doublecolumnhsize = \hsize + \advance\doublecolumnhsize by -.04154\hsize + \divide\doublecolumnhsize by 2 + \hsize = \doublecolumnhsize + % + % Double the \vsize as well. (We don't need a separate register here, + % since nobody clobbers \vsize.) + \vsize = 2\vsize +} + +% The double-column output routine for all double-column pages except +% the last. +% +\def\doublecolumnout{% + \splittopskip=\topskip \splitmaxdepth=\maxdepth + % Get the available space for the double columns -- the normal + % (undoubled) page height minus any material left over from the + % previous page. + \dimen@ = \vsize + \divide\dimen@ by 2 + \advance\dimen@ by -\ht\partialpage + % + % box0 will be the left-hand column, box2 the right. + \setbox0=\vsplit255 to\dimen@ \setbox2=\vsplit255 to\dimen@ + \onepageout\pagesofar + \unvbox255 + \penalty\outputpenalty +} +% +% Re-output the contents of the output page -- any previous material, +% followed by the two boxes we just split, in box0 and box2. +\def\pagesofar{% + \unvbox\partialpage + % + \hsize = \doublecolumnhsize + \wd0=\hsize \wd2=\hsize + \hbox to\pagewidth{\box0\hfil\box2}% +} +% +% All done with double columns. +\def\enddoublecolumns{% + % The following penalty ensures that the page builder is exercised + % _before_ we change the output routine. This is necessary in the + % following situation: + % + % The last section of the index consists only of a single entry. + % Before this section, \pagetotal is less than \pagegoal, so no + % break occurs before the last section starts. However, the last + % section, consisting of \initial and the single \entry, does not + % fit on the page and has to be broken off. Without the following + % penalty the page builder will not be exercised until \eject + % below, and by that time we'll already have changed the output + % routine to the \balancecolumns version, so the next-to-last + % double-column page will be processed with \balancecolumns, which + % is wrong: The two columns will go to the main vertical list, with + % the broken-off section in the recent contributions. As soon as + % the output routine finishes, TeX starts reconsidering the page + % break. The two columns and the broken-off section both fit on the + % page, because the two columns now take up only half of the page + % goal. When TeX sees \eject from below which follows the final + % section, it invokes the new output routine that we've set after + % \balancecolumns below; \onepageout will try to fit the two columns + % and the final section into the vbox of \pageheight (see + % \pagebody), causing an overfull box. + % + % Note that glue won't work here, because glue does not exercise the + % page builder, unlike penalties (see The TeXbook, pp. 280-281). + \penalty0 + % + \output = {% + % Split the last of the double-column material. Leave it on the + % current page, no automatic page break. + \balancecolumns + % + % If we end up splitting too much material for the current page, + % though, there will be another page break right after this \output + % invocation ends. Having called \balancecolumns once, we do not + % want to call it again. Therefore, reset \output to its normal + % definition right away. (We hope \balancecolumns will never be + % called on to balance too much material, but if it is, this makes + % the output somewhat more palatable.) + \global\output = {\onepageout{\pagecontents\PAGE}}% + }% + \eject + \endgroup % started in \begindoublecolumns + % + % \pagegoal was set to the doubled \vsize above, since we restarted + % the current page. We're now back to normal single-column + % typesetting, so reset \pagegoal to the normal \vsize (after the + % \endgroup where \vsize got restored). + \pagegoal = \vsize +} +% +% Called at the end of the double column material. +\def\balancecolumns{% + \setbox0 = \vbox{\unvbox255}% like \box255 but more efficient, see p.120. + \dimen@ = \ht0 + \advance\dimen@ by \topskip + \advance\dimen@ by-\baselineskip + \divide\dimen@ by 2 % target to split to + %debug\message{final 2-column material height=\the\ht0, target=\the\dimen@.}% + \splittopskip = \topskip + % Loop until we get a decent breakpoint. + {% + \vbadness = 10000 + \loop + \global\setbox3 = \copy0 + \global\setbox1 = \vsplit3 to \dimen@ + \ifdim\ht3>\dimen@ + \global\advance\dimen@ by 1pt + \repeat + }% + %debug\message{split to \the\dimen@, column heights: \the\ht1, \the\ht3.}% + \setbox0=\vbox to\dimen@{\unvbox1}% + \setbox2=\vbox to\dimen@{\unvbox3}% + % + \pagesofar +} +\catcode`\@ = \other + + +\message{sectioning,} +% Chapters, sections, etc. + +% \unnumberedno is an oxymoron, of course. But we count the unnumbered +% sections so that we can refer to them unambiguously in the pdf +% outlines by their "section number". We avoid collisions with chapter +% numbers by starting them at 10000. (If a document ever has 10000 +% chapters, we're in trouble anyway, I'm sure.) +\newcount\unnumberedno \unnumberedno = 10000 +\newcount\chapno +\newcount\secno \secno=0 +\newcount\subsecno \subsecno=0 +\newcount\subsubsecno \subsubsecno=0 + +% This counter is funny since it counts through charcodes of letters A, B, ... +\newcount\appendixno \appendixno = `\@ +% +% \def\appendixletter{\char\the\appendixno} +% We do the following ugly conditional instead of the above simple +% construct for the sake of pdftex, which needs the actual +% letter in the expansion, not just typeset. +% +\def\appendixletter{% + \ifnum\appendixno=`A A% + \else\ifnum\appendixno=`B B% + \else\ifnum\appendixno=`C C% + \else\ifnum\appendixno=`D D% + \else\ifnum\appendixno=`E E% + \else\ifnum\appendixno=`F F% + \else\ifnum\appendixno=`G G% + \else\ifnum\appendixno=`H H% + \else\ifnum\appendixno=`I I% + \else\ifnum\appendixno=`J J% + \else\ifnum\appendixno=`K K% + \else\ifnum\appendixno=`L L% + \else\ifnum\appendixno=`M M% + \else\ifnum\appendixno=`N N% + \else\ifnum\appendixno=`O O% + \else\ifnum\appendixno=`P P% + \else\ifnum\appendixno=`Q Q% + \else\ifnum\appendixno=`R R% + \else\ifnum\appendixno=`S S% + \else\ifnum\appendixno=`T T% + \else\ifnum\appendixno=`U U% + \else\ifnum\appendixno=`V V% + \else\ifnum\appendixno=`W W% + \else\ifnum\appendixno=`X X% + \else\ifnum\appendixno=`Y Y% + \else\ifnum\appendixno=`Z Z% + % The \the is necessary, despite appearances, because \appendixletter is + % expanded while writing the .toc file. \char\appendixno is not + % expandable, thus it is written literally, thus all appendixes come out + % with the same letter (or @) in the toc without it. + \else\char\the\appendixno + \fi\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi + \fi\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi} + +% Each @chapter defines these (using marks) as the number+name, number +% and name of the chapter. Page headings and footings can use +% these. @section does likewise. +\def\thischapter{} +\def\thischapternum{} +\def\thischaptername{} +\def\thissection{} +\def\thissectionnum{} +\def\thissectionname{} + +\newcount\absseclevel % used to calculate proper heading level +\newcount\secbase\secbase=0 % @raisesections/@lowersections modify this count + +% @raisesections: treat @section as chapter, @subsection as section, etc. +\def\raisesections{\global\advance\secbase by -1} +\let\up=\raisesections % original BFox name + +% @lowersections: treat @chapter as section, @section as subsection, etc. +\def\lowersections{\global\advance\secbase by 1} +\let\down=\lowersections % original BFox name + +% we only have subsub. +\chardef\maxseclevel = 3 +% +% A numbered section within an unnumbered changes to unnumbered too. +% To achive this, remember the "biggest" unnum. sec. we are currently in: +\chardef\unmlevel = \maxseclevel +% +% Trace whether the current chapter is an appendix or not: +% \chapheadtype is "N" or "A", unnumbered chapters are ignored. +\def\chapheadtype{N} + +% Choose a heading macro +% #1 is heading type +% #2 is heading level +% #3 is text for heading +\def\genhead#1#2#3{% + % Compute the abs. sec. level: + \absseclevel=#2 + \advance\absseclevel by \secbase + % Make sure \absseclevel doesn't fall outside the range: + \ifnum \absseclevel < 0 + \absseclevel = 0 + \else + \ifnum \absseclevel > 3 + \absseclevel = 3 + \fi + \fi + % The heading type: + \def\headtype{#1}% + \if \headtype U% + \ifnum \absseclevel < \unmlevel + \chardef\unmlevel = \absseclevel + \fi + \else + % Check for appendix sections: + \ifnum \absseclevel = 0 + \edef\chapheadtype{\headtype}% + \else + \if \headtype A\if \chapheadtype N% + \errmessage{@appendix... within a non-appendix chapter}% + \fi\fi + \fi + % Check for numbered within unnumbered: + \ifnum \absseclevel > \unmlevel + \def\headtype{U}% + \else + \chardef\unmlevel = 3 + \fi + \fi + % Now print the heading: + \if \headtype U% + \ifcase\absseclevel + \unnumberedzzz{#3}% + \or \unnumberedseczzz{#3}% + \or \unnumberedsubseczzz{#3}% + \or \unnumberedsubsubseczzz{#3}% + \fi + \else + \if \headtype A% + \ifcase\absseclevel + \appendixzzz{#3}% + \or \appendixsectionzzz{#3}% + \or \appendixsubseczzz{#3}% + \or \appendixsubsubseczzz{#3}% + \fi + \else + \ifcase\absseclevel + \chapterzzz{#3}% + \or \seczzz{#3}% + \or \numberedsubseczzz{#3}% + \or \numberedsubsubseczzz{#3}% + \fi + \fi + \fi + \suppressfirstparagraphindent +} + +% an interface: +\def\numhead{\genhead N} +\def\apphead{\genhead A} +\def\unnmhead{\genhead U} + +% @chapter, @appendix, @unnumbered. Increment top-level counter, reset +% all lower-level sectioning counters to zero. +% +% Also set \chaplevelprefix, which we prepend to @float sequence numbers +% (e.g., figures), q.v. By default (before any chapter), that is empty. +\let\chaplevelprefix = \empty +% +\outer\parseargdef\chapter{\numhead0{#1}} % normally numhead0 calls chapterzzz +\def\chapterzzz#1{% + % section resetting is \global in case the chapter is in a group, such + % as an @include file. + \global\secno=0 \global\subsecno=0 \global\subsubsecno=0 + \global\advance\chapno by 1 + % + % Used for \float. + \gdef\chaplevelprefix{\the\chapno.}% + \resetallfloatnos + % + \message{\putwordChapter\space \the\chapno}% + % + % Write the actual heading. + \chapmacro{#1}{Ynumbered}{\the\chapno}% + % + % So @section and the like are numbered underneath this chapter. + \global\let\section = \numberedsec + \global\let\subsection = \numberedsubsec + \global\let\subsubsection = \numberedsubsubsec +} + +\outer\parseargdef\appendix{\apphead0{#1}} % normally apphead0 calls appendixzzz +\def\appendixzzz#1{% + \global\secno=0 \global\subsecno=0 \global\subsubsecno=0 + \global\advance\appendixno by 1 + \gdef\chaplevelprefix{\appendixletter.}% + \resetallfloatnos + % + \def\appendixnum{\putwordAppendix\space \appendixletter}% + \message{\appendixnum}% + % + \chapmacro{#1}{Yappendix}{\appendixletter}% + % + \global\let\section = \appendixsec + \global\let\subsection = \appendixsubsec + \global\let\subsubsection = \appendixsubsubsec +} + +\outer\parseargdef\unnumbered{\unnmhead0{#1}} % normally unnmhead0 calls unnumberedzzz +\def\unnumberedzzz#1{% + \global\secno=0 \global\subsecno=0 \global\subsubsecno=0 + \global\advance\unnumberedno by 1 + % + % Since an unnumbered has no number, no prefix for figures. + \global\let\chaplevelprefix = \empty + \resetallfloatnos + % + % This used to be simply \message{#1}, but TeX fully expands the + % argument to \message. Therefore, if #1 contained @-commands, TeX + % expanded them. For example, in `@unnumbered The @cite{Book}', TeX + % expanded @cite (which turns out to cause errors because \cite is meant + % to be executed, not expanded). + % + % Anyway, we don't want the fully-expanded definition of @cite to appear + % as a result of the \message, we just want `@cite' itself. We use + % \the<toks register> to achieve this: TeX expands \the<toks> only once, + % simply yielding the contents of <toks register>. (We also do this for + % the toc entries.) + \toks0 = {#1}% + \message{(\the\toks0)}% + % + \chapmacro{#1}{Ynothing}{\the\unnumberedno}% + % + \global\let\section = \unnumberedsec + \global\let\subsection = \unnumberedsubsec + \global\let\subsubsection = \unnumberedsubsubsec +} + +% @centerchap is like @unnumbered, but the heading is centered. +\outer\parseargdef\centerchap{% + % Well, we could do the following in a group, but that would break + % an assumption that \chapmacro is called at the outermost level. + % Thus we are safer this way: --kasal, 24feb04 + \let\centerparametersmaybe = \centerparameters + \unnmhead0{#1}% + \let\centerparametersmaybe = \relax +} + +% @top is like @unnumbered. +\let\top\unnumbered + +% Sections. +\outer\parseargdef\numberedsec{\numhead1{#1}} % normally calls seczzz +\def\seczzz#1{% + \global\subsecno=0 \global\subsubsecno=0 \global\advance\secno by 1 + \sectionheading{#1}{sec}{Ynumbered}{\the\chapno.\the\secno}% +} + +\outer\parseargdef\appendixsection{\apphead1{#1}} % normally calls appendixsectionzzz +\def\appendixsectionzzz#1{% + \global\subsecno=0 \global\subsubsecno=0 \global\advance\secno by 1 + \sectionheading{#1}{sec}{Yappendix}{\appendixletter.\the\secno}% +} +\let\appendixsec\appendixsection + +\outer\parseargdef\unnumberedsec{\unnmhead1{#1}} % normally calls unnumberedseczzz +\def\unnumberedseczzz#1{% + \global\subsecno=0 \global\subsubsecno=0 \global\advance\secno by 1 + \sectionheading{#1}{sec}{Ynothing}{\the\unnumberedno.\the\secno}% +} + +% Subsections. +\outer\parseargdef\numberedsubsec{\numhead2{#1}} % normally calls numberedsubseczzz +\def\numberedsubseczzz#1{% + \global\subsubsecno=0 \global\advance\subsecno by 1 + \sectionheading{#1}{subsec}{Ynumbered}{\the\chapno.\the\secno.\the\subsecno}% +} + +\outer\parseargdef\appendixsubsec{\apphead2{#1}} % normally calls appendixsubseczzz +\def\appendixsubseczzz#1{% + \global\subsubsecno=0 \global\advance\subsecno by 1 + \sectionheading{#1}{subsec}{Yappendix}% + {\appendixletter.\the\secno.\the\subsecno}% +} + +\outer\parseargdef\unnumberedsubsec{\unnmhead2{#1}} %normally calls unnumberedsubseczzz +\def\unnumberedsubseczzz#1{% + \global\subsubsecno=0 \global\advance\subsecno by 1 + \sectionheading{#1}{subsec}{Ynothing}% + {\the\unnumberedno.\the\secno.\the\subsecno}% +} + +% Subsubsections. +\outer\parseargdef\numberedsubsubsec{\numhead3{#1}} % normally numberedsubsubseczzz +\def\numberedsubsubseczzz#1{% + \global\advance\subsubsecno by 1 + \sectionheading{#1}{subsubsec}{Ynumbered}% + {\the\chapno.\the\secno.\the\subsecno.\the\subsubsecno}% +} + +\outer\parseargdef\appendixsubsubsec{\apphead3{#1}} % normally appendixsubsubseczzz +\def\appendixsubsubseczzz#1{% + \global\advance\subsubsecno by 1 + \sectionheading{#1}{subsubsec}{Yappendix}% + {\appendixletter.\the\secno.\the\subsecno.\the\subsubsecno}% +} + +\outer\parseargdef\unnumberedsubsubsec{\unnmhead3{#1}} %normally unnumberedsubsubseczzz +\def\unnumberedsubsubseczzz#1{% + \global\advance\subsubsecno by 1 + \sectionheading{#1}{subsubsec}{Ynothing}% + {\the\unnumberedno.\the\secno.\the\subsecno.\the\subsubsecno}% +} + +% These macros control what the section commands do, according +% to what kind of chapter we are in (ordinary, appendix, or unnumbered). +% Define them by default for a numbered chapter. +\let\section = \numberedsec +\let\subsection = \numberedsubsec +\let\subsubsection = \numberedsubsubsec + +% Define @majorheading, @heading and @subheading + +% NOTE on use of \vbox for chapter headings, section headings, and such: +% 1) We use \vbox rather than the earlier \line to permit +% overlong headings to fold. +% 2) \hyphenpenalty is set to 10000 because hyphenation in a +% heading is obnoxious; this forbids it. +% 3) Likewise, headings look best if no \parindent is used, and +% if justification is not attempted. Hence \raggedright. + + +\def\majorheading{% + {\advance\chapheadingskip by 10pt \chapbreak }% + \parsearg\chapheadingzzz +} + +\def\chapheading{\chapbreak \parsearg\chapheadingzzz} +\def\chapheadingzzz#1{% + {\chapfonts \vbox{\hyphenpenalty=10000\tolerance=5000 + \parindent=0pt\raggedright + \rm #1\hfill}}% + \bigskip \par\penalty 200\relax + \suppressfirstparagraphindent +} + +% @heading, @subheading, @subsubheading. +\parseargdef\heading{\sectionheading{#1}{sec}{Yomitfromtoc}{} + \suppressfirstparagraphindent} +\parseargdef\subheading{\sectionheading{#1}{subsec}{Yomitfromtoc}{} + \suppressfirstparagraphindent} +\parseargdef\subsubheading{\sectionheading{#1}{subsubsec}{Yomitfromtoc}{} + \suppressfirstparagraphindent} + +% These macros generate a chapter, section, etc. heading only +% (including whitespace, linebreaking, etc. around it), +% given all the information in convenient, parsed form. + +%%% Args are the skip and penalty (usually negative) +\def\dobreak#1#2{\par\ifdim\lastskip<#1\removelastskip\penalty#2\vskip#1\fi} + +%%% Define plain chapter starts, and page on/off switching for it +% Parameter controlling skip before chapter headings (if needed) + +\newskip\chapheadingskip + +\def\chapbreak{\dobreak \chapheadingskip {-4000}} +\def\chappager{\par\vfill\supereject} +% Because \domark is called before \chapoddpage, the filler page will +% get the headings for the next chapter, which is wrong. But we don't +% care -- we just disable all headings on the filler page. +\def\chapoddpage{% + \chappager + \ifodd\pageno \else + \begingroup + \evenheadline={\hfil}\evenfootline={\hfil}% + \oddheadline={\hfil}\oddfootline={\hfil}% + \hbox to 0pt{}% + \chappager + \endgroup + \fi +} + +\def\setchapternewpage #1 {\csname CHAPPAG#1\endcsname} + +\def\CHAPPAGoff{% +\global\let\contentsalignmacro = \chappager +\global\let\pchapsepmacro=\chapbreak +\global\let\pagealignmacro=\chappager} + +\def\CHAPPAGon{% +\global\let\contentsalignmacro = \chappager +\global\let\pchapsepmacro=\chappager +\global\let\pagealignmacro=\chappager +\global\def\HEADINGSon{\HEADINGSsingle}} + +\def\CHAPPAGodd{% +\global\let\contentsalignmacro = \chapoddpage +\global\let\pchapsepmacro=\chapoddpage +\global\let\pagealignmacro=\chapoddpage +\global\def\HEADINGSon{\HEADINGSdouble}} + +\CHAPPAGon + +% Chapter opening. +% +% #1 is the text, #2 is the section type (Ynumbered, Ynothing, +% Yappendix, Yomitfromtoc), #3 the chapter number. +% +% To test against our argument. +\def\Ynothingkeyword{Ynothing} +\def\Yomitfromtockeyword{Yomitfromtoc} +\def\Yappendixkeyword{Yappendix} +% +\def\chapmacro#1#2#3{% + % Insert the first mark before the heading break (see notes for \domark). + \let\prevchapterdefs=\lastchapterdefs + \let\prevsectiondefs=\lastsectiondefs + \gdef\lastsectiondefs{\gdef\thissectionname{}\gdef\thissectionnum{}% + \gdef\thissection{}}% + % + \def\temptype{#2}% + \ifx\temptype\Ynothingkeyword + \gdef\lastchapterdefs{\gdef\thischaptername{#1}\gdef\thischapternum{}% + \gdef\thischapter{\thischaptername}}% + \else\ifx\temptype\Yomitfromtockeyword + \gdef\lastchapterdefs{\gdef\thischaptername{#1}\gdef\thischapternum{}% + \gdef\thischapter{}}% + \else\ifx\temptype\Yappendixkeyword + \toks0={#1}% + \xdef\lastchapterdefs{% + \gdef\noexpand\thischaptername{\the\toks0}% + \gdef\noexpand\thischapternum{\appendixletter}% + \gdef\noexpand\thischapter{\putwordAppendix{} \noexpand\thischapternum: + \noexpand\thischaptername}% + }% + \else + \toks0={#1}% + \xdef\lastchapterdefs{% + \gdef\noexpand\thischaptername{\the\toks0}% + \gdef\noexpand\thischapternum{\the\chapno}% + \gdef\noexpand\thischapter{\putwordChapter{} \noexpand\thischapternum: + \noexpand\thischaptername}% + }% + \fi\fi\fi + % + % Output the mark. Pass it through \safewhatsit, to take care of + % the preceding space. + \safewhatsit\domark + % + % Insert the chapter heading break. + \pchapsepmacro + % + % Now the second mark, after the heading break. No break points + % between here and the heading. + \let\prevchapterdefs=\lastchapterdefs + \let\prevsectiondefs=\lastsectiondefs + \domark + % + {% + \chapfonts \rm + % + % Have to define \lastsection before calling \donoderef, because the + % xref code eventually uses it. On the other hand, it has to be called + % after \pchapsepmacro, or the headline will change too soon. + \gdef\lastsection{#1}% + % + % Only insert the separating space if we have a chapter/appendix + % number, and don't print the unnumbered ``number''. + \ifx\temptype\Ynothingkeyword + \setbox0 = \hbox{}% + \def\toctype{unnchap}% + \else\ifx\temptype\Yomitfromtockeyword + \setbox0 = \hbox{}% contents like unnumbered, but no toc entry + \def\toctype{omit}% + \else\ifx\temptype\Yappendixkeyword + \setbox0 = \hbox{\putwordAppendix{} #3\enspace}% + \def\toctype{app}% + \else + \setbox0 = \hbox{#3\enspace}% + \def\toctype{numchap}% + \fi\fi\fi + % + % Write the toc entry for this chapter. Must come before the + % \donoderef, because we include the current node name in the toc + % entry, and \donoderef resets it to empty. + \writetocentry{\toctype}{#1}{#3}% + % + % For pdftex, we have to write out the node definition (aka, make + % the pdfdest) after any page break, but before the actual text has + % been typeset. If the destination for the pdf outline is after the + % text, then jumping from the outline may wind up with the text not + % being visible, for instance under high magnification. + \donoderef{#2}% + % + % Typeset the actual heading. + \nobreak % Avoid page breaks at the interline glue. + \vbox{\hyphenpenalty=10000 \tolerance=5000 \parindent=0pt \raggedright + \hangindent=\wd0 \centerparametersmaybe + \unhbox0 #1\par}% + }% + \nobreak\bigskip % no page break after a chapter title + \nobreak +} + +% @centerchap -- centered and unnumbered. +\let\centerparametersmaybe = \relax +\def\centerparameters{% + \advance\rightskip by 3\rightskip + \leftskip = \rightskip + \parfillskip = 0pt +} + + +% I don't think this chapter style is supported any more, so I'm not +% updating it with the new noderef stuff. We'll see. --karl, 11aug03. +% +\def\setchapterstyle #1 {\csname CHAPF#1\endcsname} +% +\def\unnchfopen #1{% +\chapoddpage {\chapfonts \vbox{\hyphenpenalty=10000\tolerance=5000 + \parindent=0pt\raggedright + \rm #1\hfill}}\bigskip \par\nobreak +} +\def\chfopen #1#2{\chapoddpage {\chapfonts +\vbox to 3in{\vfil \hbox to\hsize{\hfil #2} \hbox to\hsize{\hfil #1} \vfil}}% +\par\penalty 5000 % +} +\def\centerchfopen #1{% +\chapoddpage {\chapfonts \vbox{\hyphenpenalty=10000\tolerance=5000 + \parindent=0pt + \hfill {\rm #1}\hfill}}\bigskip \par\nobreak +} +\def\CHAPFopen{% + \global\let\chapmacro=\chfopen + \global\let\centerchapmacro=\centerchfopen} + + +% Section titles. These macros combine the section number parts and +% call the generic \sectionheading to do the printing. +% +\newskip\secheadingskip +\def\secheadingbreak{\dobreak \secheadingskip{-1000}} + +% Subsection titles. +\newskip\subsecheadingskip +\def\subsecheadingbreak{\dobreak \subsecheadingskip{-500}} + +% Subsubsection titles. +\def\subsubsecheadingskip{\subsecheadingskip} +\def\subsubsecheadingbreak{\subsecheadingbreak} + + +% Print any size, any type, section title. +% +% #1 is the text, #2 is the section level (sec/subsec/subsubsec), #3 is +% the section type for xrefs (Ynumbered, Ynothing, Yappendix), #4 is the +% section number. +% +\def\seckeyword{sec} +% +\def\sectionheading#1#2#3#4{% + {% + % Switch to the right set of fonts. + \csname #2fonts\endcsname \rm + % + \def\sectionlevel{#2}% + \def\temptype{#3}% + % + % Insert first mark before the heading break (see notes for \domark). + \let\prevsectiondefs=\lastsectiondefs + \ifx\temptype\Ynothingkeyword + \ifx\sectionlevel\seckeyword + \gdef\lastsectiondefs{\gdef\thissectionname{#1}\gdef\thissectionnum{}% + \gdef\thissection{\thissectionname}}% + \fi + \else\ifx\temptype\Yomitfromtockeyword + % Don't redefine \thissection. + \else\ifx\temptype\Yappendixkeyword + \ifx\sectionlevel\seckeyword + \toks0={#1}% + \xdef\lastsectiondefs{% + \gdef\noexpand\thissectionname{\the\toks0}% + \gdef\noexpand\thissectionnum{#4}% + \gdef\noexpand\thissection{\putwordSection{} \noexpand\thissectionnum: + \noexpand\thissectionname}% + }% + \fi + \else + \ifx\sectionlevel\seckeyword + \toks0={#1}% + \xdef\lastsectiondefs{% + \gdef\noexpand\thissectionname{\the\toks0}% + \gdef\noexpand\thissectionnum{#4}% + \gdef\noexpand\thissection{\putwordSection{} \noexpand\thissectionnum: + \noexpand\thissectionname}% + }% + \fi + \fi\fi\fi + % + % Output the mark. Pass it through \safewhatsit, to take care of + % the preceding space. + \safewhatsit\domark + % + % Insert space above the heading. + \csname #2headingbreak\endcsname + % + % Now the second mark, after the heading break. No break points + % between here and the heading. + \let\prevsectiondefs=\lastsectiondefs + \domark + % + % Only insert the space after the number if we have a section number. + \ifx\temptype\Ynothingkeyword + \setbox0 = \hbox{}% + \def\toctype{unn}% + \gdef\lastsection{#1}% + \else\ifx\temptype\Yomitfromtockeyword + % for @headings -- no section number, don't include in toc, + % and don't redefine \lastsection. + \setbox0 = \hbox{}% + \def\toctype{omit}% + \let\sectionlevel=\empty + \else\ifx\temptype\Yappendixkeyword + \setbox0 = \hbox{#4\enspace}% + \def\toctype{app}% + \gdef\lastsection{#1}% + \else + \setbox0 = \hbox{#4\enspace}% + \def\toctype{num}% + \gdef\lastsection{#1}% + \fi\fi\fi + % + % Write the toc entry (before \donoderef). See comments in \chapmacro. + \writetocentry{\toctype\sectionlevel}{#1}{#4}% + % + % Write the node reference (= pdf destination for pdftex). + % Again, see comments in \chapmacro. + \donoderef{#3}% + % + % Interline glue will be inserted when the vbox is completed. + % That glue will be a valid breakpoint for the page, since it'll be + % preceded by a whatsit (usually from the \donoderef, or from the + % \writetocentry if there was no node). We don't want to allow that + % break, since then the whatsits could end up on page n while the + % section is on page n+1, thus toc/etc. are wrong. Debian bug 276000. + \nobreak + % + % Output the actual section heading. + \vbox{\hyphenpenalty=10000 \tolerance=5000 \parindent=0pt \raggedright + \hangindent=\wd0 % zero if no section number + \unhbox0 #1}% + }% + % Add extra space after the heading -- half of whatever came above it. + % Don't allow stretch, though. + \kern .5 \csname #2headingskip\endcsname + % + % Do not let the kern be a potential breakpoint, as it would be if it + % was followed by glue. + \nobreak + % + % We'll almost certainly start a paragraph next, so don't let that + % glue accumulate. (Not a breakpoint because it's preceded by a + % discardable item.) + \vskip-\parskip + % + % This is purely so the last item on the list is a known \penalty > + % 10000. This is so \startdefun can avoid allowing breakpoints after + % section headings. Otherwise, it would insert a valid breakpoint between: + % + % @section sec-whatever + % @deffn def-whatever + \penalty 10001 +} + + +\message{toc,} +% Table of contents. +\newwrite\tocfile + +% Write an entry to the toc file, opening it if necessary. +% Called from @chapter, etc. +% +% Example usage: \writetocentry{sec}{Section Name}{\the\chapno.\the\secno} +% We append the current node name (if any) and page number as additional +% arguments for the \{chap,sec,...}entry macros which will eventually +% read this. The node name is used in the pdf outlines as the +% destination to jump to. +% +% We open the .toc file for writing here instead of at @setfilename (or +% any other fixed time) so that @contents can be anywhere in the document. +% But if #1 is `omit', then we don't do anything. This is used for the +% table of contents chapter openings themselves. +% +\newif\iftocfileopened +\def\omitkeyword{omit}% +% +\def\writetocentry#1#2#3{% + \edef\writetoctype{#1}% + \ifx\writetoctype\omitkeyword \else + \iftocfileopened\else + \immediate\openout\tocfile = \jobname.toc + \global\tocfileopenedtrue + \fi + % + \iflinks + {\atdummies + \edef\temp{% + \write\tocfile{@#1entry{#2}{#3}{\lastnode}{\noexpand\folio}}}% + \temp + }% + \fi + \fi + % + % Tell \shipout to create a pdf destination on each page, if we're + % writing pdf. These are used in the table of contents. We can't + % just write one on every page because the title pages are numbered + % 1 and 2 (the page numbers aren't printed), and so are the first + % two pages of the document. Thus, we'd have two destinations named + % `1', and two named `2'. + \ifpdf \global\pdfmakepagedesttrue \fi +} + + +% These characters do not print properly in the Computer Modern roman +% fonts, so we must take special care. This is more or less redundant +% with the Texinfo input format setup at the end of this file. +% +\def\activecatcodes{% + \catcode`\"=\active + \catcode`\$=\active + \catcode`\<=\active + \catcode`\>=\active + \catcode`\\=\active + \catcode`\^=\active + \catcode`\_=\active + \catcode`\|=\active + \catcode`\~=\active +} + + +% Read the toc file, which is essentially Texinfo input. +\def\readtocfile{% + \setupdatafile + \activecatcodes + \input \tocreadfilename +} + +\newskip\contentsrightmargin \contentsrightmargin=1in +\newcount\savepageno +\newcount\lastnegativepageno \lastnegativepageno = -1 + +% Prepare to read what we've written to \tocfile. +% +\def\startcontents#1{% + % If @setchapternewpage on, and @headings double, the contents should + % start on an odd page, unlike chapters. Thus, we maintain + % \contentsalignmacro in parallel with \pagealignmacro. + % From: Torbjorn Granlund <tege@matematik.su.se> + \contentsalignmacro + \immediate\closeout\tocfile + % + % Don't need to put `Contents' or `Short Contents' in the headline. + % It is abundantly clear what they are. + \chapmacro{#1}{Yomitfromtoc}{}% + % + \savepageno = \pageno + \begingroup % Set up to handle contents files properly. + \raggedbottom % Worry more about breakpoints than the bottom. + \advance\hsize by -\contentsrightmargin % Don't use the full line length. + % + % Roman numerals for page numbers. + \ifnum \pageno>0 \global\pageno = \lastnegativepageno \fi +} + +% redefined for the two-volume lispref. We always output on +% \jobname.toc even if this is redefined. +% +\def\tocreadfilename{\jobname.toc} + +% Normal (long) toc. +% +\def\contents{% + \startcontents{\putwordTOC}% + \openin 1 \tocreadfilename\space + \ifeof 1 \else + \readtocfile + \fi + \vfill \eject + \contentsalignmacro % in case @setchapternewpage odd is in effect + \ifeof 1 \else + \pdfmakeoutlines + \fi + \closein 1 + \endgroup + \lastnegativepageno = \pageno + \global\pageno = \savepageno +} + +% And just the chapters. +\def\summarycontents{% + \startcontents{\putwordShortTOC}% + % + \let\numchapentry = \shortchapentry + \let\appentry = \shortchapentry + \let\unnchapentry = \shortunnchapentry + % We want a true roman here for the page numbers. + \secfonts + \let\rm=\shortcontrm \let\bf=\shortcontbf + \let\sl=\shortcontsl \let\tt=\shortconttt + \rm + \hyphenpenalty = 10000 + \advance\baselineskip by 1pt % Open it up a little. + \def\numsecentry##1##2##3##4{} + \let\appsecentry = \numsecentry + \let\unnsecentry = \numsecentry + \let\numsubsecentry = \numsecentry + \let\appsubsecentry = \numsecentry + \let\unnsubsecentry = \numsecentry + \let\numsubsubsecentry = \numsecentry + \let\appsubsubsecentry = \numsecentry + \let\unnsubsubsecentry = \numsecentry + \openin 1 \tocreadfilename\space + \ifeof 1 \else + \readtocfile + \fi + \closein 1 + \vfill \eject + \contentsalignmacro % in case @setchapternewpage odd is in effect + \endgroup + \lastnegativepageno = \pageno + \global\pageno = \savepageno +} +\let\shortcontents = \summarycontents + +% Typeset the label for a chapter or appendix for the short contents. +% The arg is, e.g., `A' for an appendix, or `3' for a chapter. +% +\def\shortchaplabel#1{% + % This space should be enough, since a single number is .5em, and the + % widest letter (M) is 1em, at least in the Computer Modern fonts. + % But use \hss just in case. + % (This space doesn't include the extra space that gets added after + % the label; that gets put in by \shortchapentry above.) + % + % We'd like to right-justify chapter numbers, but that looks strange + % with appendix letters. And right-justifying numbers and + % left-justifying letters looks strange when there is less than 10 + % chapters. Have to read the whole toc once to know how many chapters + % there are before deciding ... + \hbox to 1em{#1\hss}% +} + +% These macros generate individual entries in the table of contents. +% The first argument is the chapter or section name. +% The last argument is the page number. +% The arguments in between are the chapter number, section number, ... + +% Chapters, in the main contents. +\def\numchapentry#1#2#3#4{\dochapentry{#2\labelspace#1}{#4}} +% +% Chapters, in the short toc. +% See comments in \dochapentry re vbox and related settings. +\def\shortchapentry#1#2#3#4{% + \tocentry{\shortchaplabel{#2}\labelspace #1}{\doshortpageno\bgroup#4\egroup}% +} + +% Appendices, in the main contents. +% Need the word Appendix, and a fixed-size box. +% +\def\appendixbox#1{% + % We use M since it's probably the widest letter. + \setbox0 = \hbox{\putwordAppendix{} M}% + \hbox to \wd0{\putwordAppendix{} #1\hss}} +% +\def\appentry#1#2#3#4{\dochapentry{\appendixbox{#2}\labelspace#1}{#4}} + +% Unnumbered chapters. +\def\unnchapentry#1#2#3#4{\dochapentry{#1}{#4}} +\def\shortunnchapentry#1#2#3#4{\tocentry{#1}{\doshortpageno\bgroup#4\egroup}} + +% Sections. +\def\numsecentry#1#2#3#4{\dosecentry{#2\labelspace#1}{#4}} +\let\appsecentry=\numsecentry +\def\unnsecentry#1#2#3#4{\dosecentry{#1}{#4}} + +% Subsections. +\def\numsubsecentry#1#2#3#4{\dosubsecentry{#2\labelspace#1}{#4}} +\let\appsubsecentry=\numsubsecentry +\def\unnsubsecentry#1#2#3#4{\dosubsecentry{#1}{#4}} + +% And subsubsections. +\def\numsubsubsecentry#1#2#3#4{\dosubsubsecentry{#2\labelspace#1}{#4}} +\let\appsubsubsecentry=\numsubsubsecentry +\def\unnsubsubsecentry#1#2#3#4{\dosubsubsecentry{#1}{#4}} + +% This parameter controls the indentation of the various levels. +% Same as \defaultparindent. +\newdimen\tocindent \tocindent = 15pt + +% Now for the actual typesetting. In all these, #1 is the text and #2 is the +% page number. +% +% If the toc has to be broken over pages, we want it to be at chapters +% if at all possible; hence the \penalty. +\def\dochapentry#1#2{% + \penalty-300 \vskip1\baselineskip plus.33\baselineskip minus.25\baselineskip + \begingroup + \chapentryfonts + \tocentry{#1}{\dopageno\bgroup#2\egroup}% + \endgroup + \nobreak\vskip .25\baselineskip plus.1\baselineskip +} + +\def\dosecentry#1#2{\begingroup + \secentryfonts \leftskip=\tocindent + \tocentry{#1}{\dopageno\bgroup#2\egroup}% +\endgroup} + +\def\dosubsecentry#1#2{\begingroup + \subsecentryfonts \leftskip=2\tocindent + \tocentry{#1}{\dopageno\bgroup#2\egroup}% +\endgroup} + +\def\dosubsubsecentry#1#2{\begingroup + \subsubsecentryfonts \leftskip=3\tocindent + \tocentry{#1}{\dopageno\bgroup#2\egroup}% +\endgroup} + +% We use the same \entry macro as for the index entries. +\let\tocentry = \entry + +% Space between chapter (or whatever) number and the title. +\def\labelspace{\hskip1em \relax} + +\def\dopageno#1{{\rm #1}} +\def\doshortpageno#1{{\rm #1}} + +\def\chapentryfonts{\secfonts \rm} +\def\secentryfonts{\textfonts} +\def\subsecentryfonts{\textfonts} +\def\subsubsecentryfonts{\textfonts} + + +\message{environments,} +% @foo ... @end foo. + +% @point{}, @result{}, @expansion{}, @print{}, @equiv{}. +% +% Since these characters are used in examples, it should be an even number of +% \tt widths. Each \tt character is 1en, so two makes it 1em. +% +\def\point{$\star$} +\def\result{\leavevmode\raise.15ex\hbox to 1em{\hfil$\Rightarrow$\hfil}} +\def\expansion{\leavevmode\raise.1ex\hbox to 1em{\hfil$\mapsto$\hfil}} +\def\print{\leavevmode\lower.1ex\hbox to 1em{\hfil$\dashv$\hfil}} +\def\equiv{\leavevmode\lower.1ex\hbox to 1em{\hfil$\ptexequiv$\hfil}} + +% The @error{} command. +% Adapted from the TeXbook's \boxit. +% +\newbox\errorbox +% +{\tentt \global\dimen0 = 3em}% Width of the box. +\dimen2 = .55pt % Thickness of rules +% The text. (`r' is open on the right, `e' somewhat less so on the left.) +\setbox0 = \hbox{\kern-.75pt \reducedsf error\kern-1.5pt} +% +\setbox\errorbox=\hbox to \dimen0{\hfil + \hsize = \dimen0 \advance\hsize by -5.8pt % Space to left+right. + \advance\hsize by -2\dimen2 % Rules. + \vbox{% + \hrule height\dimen2 + \hbox{\vrule width\dimen2 \kern3pt % Space to left of text. + \vtop{\kern2.4pt \box0 \kern2.4pt}% Space above/below. + \kern3pt\vrule width\dimen2}% Space to right. + \hrule height\dimen2} + \hfil} +% +\def\error{\leavevmode\lower.7ex\copy\errorbox} + +% @tex ... @end tex escapes into raw Tex temporarily. +% One exception: @ is still an escape character, so that @end tex works. +% But \@ or @@ will get a plain tex @ character. + +\envdef\tex{% + \catcode `\\=0 \catcode `\{=1 \catcode `\}=2 + \catcode `\$=3 \catcode `\&=4 \catcode `\#=6 + \catcode `\^=7 \catcode `\_=8 \catcode `\~=\active \let~=\tie + \catcode `\%=14 + \catcode `\+=\other + \catcode `\"=\other + \catcode `\|=\other + \catcode `\<=\other + \catcode `\>=\other + \escapechar=`\\ + % + \let\b=\ptexb + \let\bullet=\ptexbullet + \let\c=\ptexc + \let\,=\ptexcomma + \let\.=\ptexdot + \let\dots=\ptexdots + \let\equiv=\ptexequiv + \let\!=\ptexexclam + \let\i=\ptexi + \let\indent=\ptexindent + \let\noindent=\ptexnoindent + \let\{=\ptexlbrace + \let\+=\tabalign + \let\}=\ptexrbrace + \let\/=\ptexslash + \let\*=\ptexstar + \let\t=\ptext + \let\frenchspacing=\plainfrenchspacing + % + \def\endldots{\mathinner{\ldots\ldots\ldots\ldots}}% + \def\enddots{\relax\ifmmode\endldots\else$\mathsurround=0pt \endldots\,$\fi}% + \def\@{@}% +} +% There is no need to define \Etex. + +% Define @lisp ... @end lisp. +% @lisp environment forms a group so it can rebind things, +% including the definition of @end lisp (which normally is erroneous). + +% Amount to narrow the margins by for @lisp. +\newskip\lispnarrowing \lispnarrowing=0.4in + +% This is the definition that ^^M gets inside @lisp, @example, and other +% such environments. \null is better than a space, since it doesn't +% have any width. +\def\lisppar{\null\endgraf} + +% This space is always present above and below environments. +\newskip\envskipamount \envskipamount = 0pt + +% Make spacing and below environment symmetrical. We use \parskip here +% to help in doing that, since in @example-like environments \parskip +% is reset to zero; thus the \afterenvbreak inserts no space -- but the +% start of the next paragraph will insert \parskip. +% +\def\aboveenvbreak{{% + % =10000 instead of <10000 because of a special case in \itemzzz and + % \sectionheading, q.v. + \ifnum \lastpenalty=10000 \else + \advance\envskipamount by \parskip + \endgraf + \ifdim\lastskip<\envskipamount + \removelastskip + % it's not a good place to break if the last penalty was \nobreak + % or better ... + \ifnum\lastpenalty<10000 \penalty-50 \fi + \vskip\envskipamount + \fi + \fi +}} + +\let\afterenvbreak = \aboveenvbreak + +% \nonarrowing is a flag. If "set", @lisp etc don't narrow margins; it will +% also clear it, so that its embedded environments do the narrowing again. +\let\nonarrowing=\relax + +% @cartouche ... @end cartouche: draw rectangle w/rounded corners around +% environment contents. +\font\circle=lcircle10 +\newdimen\circthick +\newdimen\cartouter\newdimen\cartinner +\newskip\normbskip\newskip\normpskip\newskip\normlskip +\circthick=\fontdimen8\circle +% +\def\ctl{{\circle\char'013\hskip -6pt}}% 6pt from pl file: 1/2charwidth +\def\ctr{{\hskip 6pt\circle\char'010}} +\def\cbl{{\circle\char'012\hskip -6pt}} +\def\cbr{{\hskip 6pt\circle\char'011}} +\def\carttop{\hbox to \cartouter{\hskip\lskip + \ctl\leaders\hrule height\circthick\hfil\ctr + \hskip\rskip}} +\def\cartbot{\hbox to \cartouter{\hskip\lskip + \cbl\leaders\hrule height\circthick\hfil\cbr + \hskip\rskip}} +% +\newskip\lskip\newskip\rskip + +\envdef\cartouche{% + \ifhmode\par\fi % can't be in the midst of a paragraph. + \startsavinginserts + \lskip=\leftskip \rskip=\rightskip + \leftskip=0pt\rightskip=0pt % we want these *outside*. + \cartinner=\hsize \advance\cartinner by-\lskip + \advance\cartinner by-\rskip + \cartouter=\hsize + \advance\cartouter by 18.4pt % allow for 3pt kerns on either + % side, and for 6pt waste from + % each corner char, and rule thickness + \normbskip=\baselineskip \normpskip=\parskip \normlskip=\lineskip + % Flag to tell @lisp, etc., not to narrow margin. + \let\nonarrowing = t% + \vbox\bgroup + \baselineskip=0pt\parskip=0pt\lineskip=0pt + \carttop + \hbox\bgroup + \hskip\lskip + \vrule\kern3pt + \vbox\bgroup + \kern3pt + \hsize=\cartinner + \baselineskip=\normbskip + \lineskip=\normlskip + \parskip=\normpskip + \vskip -\parskip + \comment % For explanation, see the end of \def\group. +} +\def\Ecartouche{% + \ifhmode\par\fi + \kern3pt + \egroup + \kern3pt\vrule + \hskip\rskip + \egroup + \cartbot + \egroup + \checkinserts +} + + +% This macro is called at the beginning of all the @example variants, +% inside a group. +\def\nonfillstart{% + \aboveenvbreak + \hfuzz = 12pt % Don't be fussy + \sepspaces % Make spaces be word-separators rather than space tokens. + \let\par = \lisppar % don't ignore blank lines + \obeylines % each line of input is a line of output + \parskip = 0pt + \parindent = 0pt + \emergencystretch = 0pt % don't try to avoid overfull boxes + \ifx\nonarrowing\relax + \advance \leftskip by \lispnarrowing + \exdentamount=\lispnarrowing + \else + \let\nonarrowing = \relax + \fi + \let\exdent=\nofillexdent +} + +% If you want all examples etc. small: @set dispenvsize small. +% If you want even small examples the full size: @set dispenvsize nosmall. +% This affects the following displayed environments: +% @example, @display, @format, @lisp +% +\def\smallword{small} +\def\nosmallword{nosmall} +\let\SETdispenvsize\relax +\def\setnormaldispenv{% + \ifx\SETdispenvsize\smallword + % end paragraph for sake of leading, in case document has no blank + % line. This is redundant with what happens in \aboveenvbreak, but + % we need to do it before changing the fonts, and it's inconvenient + % to change the fonts afterward. + \ifnum \lastpenalty=10000 \else \endgraf \fi + \smallexamplefonts \rm + \fi +} +\def\setsmalldispenv{% + \ifx\SETdispenvsize\nosmallword + \else + \ifnum \lastpenalty=10000 \else \endgraf \fi + \smallexamplefonts \rm + \fi +} + +% We often define two environments, @foo and @smallfoo. +% Let's do it by one command: +\def\makedispenv #1#2{ + \expandafter\envdef\csname#1\endcsname {\setnormaldispenv #2} + \expandafter\envdef\csname small#1\endcsname {\setsmalldispenv #2} + \expandafter\let\csname E#1\endcsname \afterenvbreak + \expandafter\let\csname Esmall#1\endcsname \afterenvbreak +} + +% Define two synonyms: +\def\maketwodispenvs #1#2#3{ + \makedispenv{#1}{#3} + \makedispenv{#2}{#3} +} + +% @lisp: indented, narrowed, typewriter font; @example: same as @lisp. +% +% @smallexample and @smalllisp: use smaller fonts. +% Originally contributed by Pavel@xerox. +% +\maketwodispenvs {lisp}{example}{% + \nonfillstart + \tt\quoteexpand + \let\kbdfont = \kbdexamplefont % Allow @kbd to do something special. + \gobble % eat return +} +% @display/@smalldisplay: same as @lisp except keep current font. +% +\makedispenv {display}{% + \nonfillstart + \gobble +} + +% @format/@smallformat: same as @display except don't narrow margins. +% +\makedispenv{format}{% + \let\nonarrowing = t% + \nonfillstart + \gobble +} + +% @flushleft: same as @format, but doesn't obey \SETdispenvsize. +\envdef\flushleft{% + \let\nonarrowing = t% + \nonfillstart + \gobble +} +\let\Eflushleft = \afterenvbreak + +% @flushright. +% +\envdef\flushright{% + \let\nonarrowing = t% + \nonfillstart + \advance\leftskip by 0pt plus 1fill + \gobble +} +\let\Eflushright = \afterenvbreak + + +% @quotation does normal linebreaking (hence we can't use \nonfillstart) +% and narrows the margins. We keep \parskip nonzero in general, since +% we're doing normal filling. So, when using \aboveenvbreak and +% \afterenvbreak, temporarily make \parskip 0. +% +\envdef\quotation{% + {\parskip=0pt \aboveenvbreak}% because \aboveenvbreak inserts \parskip + \parindent=0pt + % + % @cartouche defines \nonarrowing to inhibit narrowing at next level down. + \ifx\nonarrowing\relax + \advance\leftskip by \lispnarrowing + \advance\rightskip by \lispnarrowing + \exdentamount = \lispnarrowing + \else + \let\nonarrowing = \relax + \fi + \parsearg\quotationlabel +} + +% We have retained a nonzero parskip for the environment, since we're +% doing normal filling. +% +\def\Equotation{% + \par + \ifx\quotationauthor\undefined\else + % indent a bit. + \leftline{\kern 2\leftskip \sl ---\quotationauthor}% + \fi + {\parskip=0pt \afterenvbreak}% +} + +% If we're given an argument, typeset it in bold with a colon after. +\def\quotationlabel#1{% + \def\temp{#1}% + \ifx\temp\empty \else + {\bf #1: }% + \fi +} + + +% LaTeX-like @verbatim...@end verbatim and @verb{<char>...<char>} +% If we want to allow any <char> as delimiter, +% we need the curly braces so that makeinfo sees the @verb command, eg: +% `@verbx...x' would look like the '@verbx' command. --janneke@gnu.org +% +% [Knuth]: Donald Ervin Knuth, 1996. The TeXbook. +% +% [Knuth] p.344; only we need to do the other characters Texinfo sets +% active too. Otherwise, they get lost as the first character on a +% verbatim line. +\def\dospecials{% + \do\ \do\\\do\{\do\}\do\$\do\&% + \do\#\do\^\do\^^K\do\_\do\^^A\do\%\do\~% + \do\<\do\>\do\|\do\@\do+\do\"% +} +% +% [Knuth] p. 380 +\def\uncatcodespecials{% + \def\do##1{\catcode`##1=\other}\dospecials} +% +% [Knuth] pp. 380,381,391 +% Disable Spanish ligatures ?` and !` of \tt font +\begingroup + \catcode`\`=\active\gdef`{\relax\lq} +\endgroup +% +% Setup for the @verb command. +% +% Eight spaces for a tab +\begingroup + \catcode`\^^I=\active + \gdef\tabeightspaces{\catcode`\^^I=\active\def^^I{\ \ \ \ \ \ \ \ }} +\endgroup +% +\def\setupverb{% + \tt % easiest (and conventionally used) font for verbatim + \def\par{\leavevmode\endgraf}% + \catcode`\`=\active + \tabeightspaces + % Respect line breaks, + % print special symbols as themselves, and + % make each space count + % must do in this order: + \obeylines \uncatcodespecials \sepspaces +} + +% Setup for the @verbatim environment +% +% Real tab expansion +\newdimen\tabw \setbox0=\hbox{\tt\space} \tabw=8\wd0 % tab amount +% +\def\starttabbox{\setbox0=\hbox\bgroup} + +% Allow an option to not replace quotes with a regular directed right +% quote/apostrophe (char 0x27), but instead use the undirected quote +% from cmtt (char 0x0d). The undirected quote is ugly, so don't make it +% the default, but it works for pasting with more pdf viewers (at least +% evince), the lilypond developers report. xpdf does work with the +% regular 0x27. +% +\def\codequoteright{% + \expandafter\ifx\csname SETtxicodequoteundirected\endcsname\relax + \expandafter\ifx\csname SETcodequoteundirected\endcsname\relax + '% + \else \char'15 \fi + \else \char'15 \fi +} +% +% and a similar option for the left quote char vs. a grave accent. +% Modern fonts display ASCII 0x60 as a grave accent, so some people like +% the code environments to do likewise. +% +\def\codequoteleft{% + \expandafter\ifx\csname SETtxicodequotebacktick\endcsname\relax + \expandafter\ifx\csname SETcodequotebacktick\endcsname\relax + `% + \else \char'22 \fi + \else \char'22 \fi +} +% +\begingroup + \catcode`\^^I=\active + \gdef\tabexpand{% + \catcode`\^^I=\active + \def^^I{\leavevmode\egroup + \dimen0=\wd0 % the width so far, or since the previous tab + \divide\dimen0 by\tabw + \multiply\dimen0 by\tabw % compute previous multiple of \tabw + \advance\dimen0 by\tabw % advance to next multiple of \tabw + \wd0=\dimen0 \box0 \starttabbox + }% + } + \catcode`\'=\active + \gdef\rquoteexpand{\catcode\rquoteChar=\active \def'{\codequoteright}}% + % + \catcode`\`=\active + \gdef\lquoteexpand{\catcode\lquoteChar=\active \def`{\codequoteleft}}% + % + \gdef\quoteexpand{\rquoteexpand \lquoteexpand}% +\endgroup + +% start the verbatim environment. +\def\setupverbatim{% + \let\nonarrowing = t% + \nonfillstart + % Easiest (and conventionally used) font for verbatim + \tt + \def\par{\leavevmode\egroup\box0\endgraf}% + \catcode`\`=\active + \tabexpand + \quoteexpand + % Respect line breaks, + % print special symbols as themselves, and + % make each space count + % must do in this order: + \obeylines \uncatcodespecials \sepspaces + \everypar{\starttabbox}% +} + +% Do the @verb magic: verbatim text is quoted by unique +% delimiter characters. Before first delimiter expect a +% right brace, after last delimiter expect closing brace: +% +% \def\doverb'{'<char>#1<char>'}'{#1} +% +% [Knuth] p. 382; only eat outer {} +\begingroup + \catcode`[=1\catcode`]=2\catcode`\{=\other\catcode`\}=\other + \gdef\doverb{#1[\def\next##1#1}[##1\endgroup]\next] +\endgroup +% +\def\verb{\begingroup\setupverb\doverb} +% +% +% Do the @verbatim magic: define the macro \doverbatim so that +% the (first) argument ends when '@end verbatim' is reached, ie: +% +% \def\doverbatim#1@end verbatim{#1} +% +% For Texinfo it's a lot easier than for LaTeX, +% because texinfo's \verbatim doesn't stop at '\end{verbatim}': +% we need not redefine '\', '{' and '}'. +% +% Inspired by LaTeX's verbatim command set [latex.ltx] +% +\begingroup + \catcode`\ =\active + \obeylines % + % ignore everything up to the first ^^M, that's the newline at the end + % of the @verbatim input line itself. Otherwise we get an extra blank + % line in the output. + \xdef\doverbatim#1^^M#2@end verbatim{#2\noexpand\end\gobble verbatim}% + % We really want {...\end verbatim} in the body of the macro, but + % without the active space; thus we have to use \xdef and \gobble. +\endgroup +% +\envdef\verbatim{% + \setupverbatim\doverbatim +} +\let\Everbatim = \afterenvbreak + + +% @verbatiminclude FILE - insert text of file in verbatim environment. +% +\def\verbatiminclude{\parseargusing\filenamecatcodes\doverbatiminclude} +% +\def\doverbatiminclude#1{% + {% + \makevalueexpandable + \setupverbatim + \input #1 + \afterenvbreak + }% +} + +% @copying ... @end copying. +% Save the text away for @insertcopying later. +% +% We save the uninterpreted tokens, rather than creating a box. +% Saving the text in a box would be much easier, but then all the +% typesetting commands (@smallbook, font changes, etc.) have to be done +% beforehand -- and a) we want @copying to be done first in the source +% file; b) letting users define the frontmatter in as flexible order as +% possible is very desirable. +% +\def\copying{\checkenv{}\begingroup\scanargctxt\docopying} +\def\docopying#1@end copying{\endgroup\def\copyingtext{#1}} +% +\def\insertcopying{% + \begingroup + \parindent = 0pt % paragraph indentation looks wrong on title page + \scanexp\copyingtext + \endgroup +} + + +\message{defuns,} +% @defun etc. + +\newskip\defbodyindent \defbodyindent=.4in +\newskip\defargsindent \defargsindent=50pt +\newskip\deflastargmargin \deflastargmargin=18pt +\newcount\defunpenalty + +% Start the processing of @deffn: +\def\startdefun{% + \ifnum\lastpenalty<10000 + \medbreak + \defunpenalty=10003 % Will keep this @deffn together with the + % following @def command, see below. + \else + % If there are two @def commands in a row, we'll have a \nobreak, + % which is there to keep the function description together with its + % header. But if there's nothing but headers, we need to allow a + % break somewhere. Check specifically for penalty 10002, inserted + % by \printdefunline, instead of 10000, since the sectioning + % commands also insert a nobreak penalty, and we don't want to allow + % a break between a section heading and a defun. + % + % As a minor refinement, we avoid "club" headers by signalling + % with penalty of 10003 after the very first @deffn in the + % sequence (see above), and penalty of 10002 after any following + % @def command. + \ifnum\lastpenalty=10002 \penalty2000 \else \defunpenalty=10002 \fi + % + % Similarly, after a section heading, do not allow a break. + % But do insert the glue. + \medskip % preceded by discardable penalty, so not a breakpoint + \fi + % + \parindent=0in + \advance\leftskip by \defbodyindent + \exdentamount=\defbodyindent +} + +\def\dodefunx#1{% + % First, check whether we are in the right environment: + \checkenv#1% + % + % As above, allow line break if we have multiple x headers in a row. + % It's not a great place, though. + \ifnum\lastpenalty=10002 \penalty3000 \else \defunpenalty=10002 \fi + % + % And now, it's time to reuse the body of the original defun: + \expandafter\gobbledefun#1% +} +\def\gobbledefun#1\startdefun{} + +% \printdefunline \deffnheader{text} +% +\def\printdefunline#1#2{% + \begingroup + % call \deffnheader: + #1#2 \endheader + % common ending: + \interlinepenalty = 10000 + \advance\rightskip by 0pt plus 1fil + \endgraf + \nobreak\vskip -\parskip + \penalty\defunpenalty % signal to \startdefun and \dodefunx + % Some of the @defun-type tags do not enable magic parentheses, + % rendering the following check redundant. But we don't optimize. + \checkparencounts + \endgroup +} + +\def\Edefun{\endgraf\medbreak} + +% \makedefun{deffn} creates \deffn, \deffnx and \Edeffn; +% the only thing remainnig is to define \deffnheader. +% +\def\makedefun#1{% + \expandafter\let\csname E#1\endcsname = \Edefun + \edef\temp{\noexpand\domakedefun + \makecsname{#1}\makecsname{#1x}\makecsname{#1header}}% + \temp +} + +% \domakedefun \deffn \deffnx \deffnheader +% +% Define \deffn and \deffnx, without parameters. +% \deffnheader has to be defined explicitly. +% +\def\domakedefun#1#2#3{% + \envdef#1{% + \startdefun + \parseargusing\activeparens{\printdefunline#3}% + }% + \def#2{\dodefunx#1}% + \def#3% +} + +%%% Untyped functions: + +% @deffn category name args +\makedefun{deffn}{\deffngeneral{}} + +% @deffn category class name args +\makedefun{defop}#1 {\defopon{#1\ \putwordon}} + +% \defopon {category on}class name args +\def\defopon#1#2 {\deffngeneral{\putwordon\ \code{#2}}{#1\ \code{#2}} } + +% \deffngeneral {subind}category name args +% +\def\deffngeneral#1#2 #3 #4\endheader{% + % Remember that \dosubind{fn}{foo}{} is equivalent to \doind{fn}{foo}. + \dosubind{fn}{\code{#3}}{#1}% + \defname{#2}{}{#3}\magicamp\defunargs{#4\unskip}% +} + +%%% Typed functions: + +% @deftypefn category type name args +\makedefun{deftypefn}{\deftypefngeneral{}} + +% @deftypeop category class type name args +\makedefun{deftypeop}#1 {\deftypeopon{#1\ \putwordon}} + +% \deftypeopon {category on}class type name args +\def\deftypeopon#1#2 {\deftypefngeneral{\putwordon\ \code{#2}}{#1\ \code{#2}} } + +% \deftypefngeneral {subind}category type name args +% +\def\deftypefngeneral#1#2 #3 #4 #5\endheader{% + \dosubind{fn}{\code{#4}}{#1}% + \defname{#2}{#3}{#4}\defunargs{#5\unskip}% +} + +%%% Typed variables: + +% @deftypevr category type var args +\makedefun{deftypevr}{\deftypecvgeneral{}} + +% @deftypecv category class type var args +\makedefun{deftypecv}#1 {\deftypecvof{#1\ \putwordof}} + +% \deftypecvof {category of}class type var args +\def\deftypecvof#1#2 {\deftypecvgeneral{\putwordof\ \code{#2}}{#1\ \code{#2}} } + +% \deftypecvgeneral {subind}category type var args +% +\def\deftypecvgeneral#1#2 #3 #4 #5\endheader{% + \dosubind{vr}{\code{#4}}{#1}% + \defname{#2}{#3}{#4}\defunargs{#5\unskip}% +} + +%%% Untyped variables: + +% @defvr category var args +\makedefun{defvr}#1 {\deftypevrheader{#1} {} } + +% @defcv category class var args +\makedefun{defcv}#1 {\defcvof{#1\ \putwordof}} + +% \defcvof {category of}class var args +\def\defcvof#1#2 {\deftypecvof{#1}#2 {} } + +%%% Type: +% @deftp category name args +\makedefun{deftp}#1 #2 #3\endheader{% + \doind{tp}{\code{#2}}% + \defname{#1}{}{#2}\defunargs{#3\unskip}% +} + +% Remaining @defun-like shortcuts: +\makedefun{defun}{\deffnheader{\putwordDeffunc} } +\makedefun{defmac}{\deffnheader{\putwordDefmac} } +\makedefun{defspec}{\deffnheader{\putwordDefspec} } +\makedefun{deftypefun}{\deftypefnheader{\putwordDeffunc} } +\makedefun{defvar}{\defvrheader{\putwordDefvar} } +\makedefun{defopt}{\defvrheader{\putwordDefopt} } +\makedefun{deftypevar}{\deftypevrheader{\putwordDefvar} } +\makedefun{defmethod}{\defopon\putwordMethodon} +\makedefun{deftypemethod}{\deftypeopon\putwordMethodon} +\makedefun{defivar}{\defcvof\putwordInstanceVariableof} +\makedefun{deftypeivar}{\deftypecvof\putwordInstanceVariableof} + +% \defname, which formats the name of the @def (not the args). +% #1 is the category, such as "Function". +% #2 is the return type, if any. +% #3 is the function name. +% +% We are followed by (but not passed) the arguments, if any. +% +\def\defname#1#2#3{% + % Get the values of \leftskip and \rightskip as they were outside the @def... + \advance\leftskip by -\defbodyindent + % + % How we'll format the type name. Putting it in brackets helps + % distinguish it from the body text that may end up on the next line + % just below it. + \def\temp{#1}% + \setbox0=\hbox{\kern\deflastargmargin \ifx\temp\empty\else [\rm\temp]\fi} + % + % Figure out line sizes for the paragraph shape. + % The first line needs space for \box0; but if \rightskip is nonzero, + % we need only space for the part of \box0 which exceeds it: + \dimen0=\hsize \advance\dimen0 by -\wd0 \advance\dimen0 by \rightskip + % The continuations: + \dimen2=\hsize \advance\dimen2 by -\defargsindent + % (plain.tex says that \dimen1 should be used only as global.) + \parshape 2 0in \dimen0 \defargsindent \dimen2 + % + % Put the type name to the right margin. + \noindent + \hbox to 0pt{% + \hfil\box0 \kern-\hsize + % \hsize has to be shortened this way: + \kern\leftskip + % Intentionally do not respect \rightskip, since we need the space. + }% + % + % Allow all lines to be underfull without complaint: + \tolerance=10000 \hbadness=10000 + \exdentamount=\defbodyindent + {% + % defun fonts. We use typewriter by default (used to be bold) because: + % . we're printing identifiers, they should be in tt in principle. + % . in languages with many accents, such as Czech or French, it's + % common to leave accents off identifiers. The result looks ok in + % tt, but exceedingly strange in rm. + % . we don't want -- and --- to be treated as ligatures. + % . this still does not fix the ?` and !` ligatures, but so far no + % one has made identifiers using them :). + \df \tt + \def\temp{#2}% return value type + \ifx\temp\empty\else \tclose{\temp} \fi + #3% output function name + }% + {\rm\enskip}% hskip 0.5 em of \tenrm + % + \boldbrax + % arguments will be output next, if any. +} + +% Print arguments in slanted roman (not ttsl), inconsistently with using +% tt for the name. This is because literal text is sometimes needed in +% the argument list (groff manual), and ttsl and tt are not very +% distinguishable. Prevent hyphenation at `-' chars. +% +\def\defunargs#1{% + % use sl by default (not ttsl), + % tt for the names. + \df \sl \hyphenchar\font=0 + % + % On the other hand, if an argument has two dashes (for instance), we + % want a way to get ttsl. Let's try @var for that. + \let\var=\ttslanted + #1% + \sl\hyphenchar\font=45 +} + +% We want ()&[] to print specially on the defun line. +% +\def\activeparens{% + \catcode`\(=\active \catcode`\)=\active + \catcode`\[=\active \catcode`\]=\active + \catcode`\&=\active +} + +% Make control sequences which act like normal parenthesis chars. +\let\lparen = ( \let\rparen = ) + +% Be sure that we always have a definition for `(', etc. For example, +% if the fn name has parens in it, \boldbrax will not be in effect yet, +% so TeX would otherwise complain about undefined control sequence. +{ + \activeparens + \global\let(=\lparen \global\let)=\rparen + \global\let[=\lbrack \global\let]=\rbrack + \global\let& = \& + + \gdef\boldbrax{\let(=\opnr\let)=\clnr\let[=\lbrb\let]=\rbrb} + \gdef\magicamp{\let&=\amprm} +} + +\newcount\parencount + +% If we encounter &foo, then turn on ()-hacking afterwards +\newif\ifampseen +\def\amprm#1 {\ampseentrue{\bf\ }} + +\def\parenfont{% + \ifampseen + % At the first level, print parens in roman, + % otherwise use the default font. + \ifnum \parencount=1 \rm \fi + \else + % The \sf parens (in \boldbrax) actually are a little bolder than + % the contained text. This is especially needed for [ and ] . + \sf + \fi +} +\def\infirstlevel#1{% + \ifampseen + \ifnum\parencount=1 + #1% + \fi + \fi +} +\def\bfafterword#1 {#1 \bf} + +\def\opnr{% + \global\advance\parencount by 1 + {\parenfont(}% + \infirstlevel \bfafterword +} +\def\clnr{% + {\parenfont)}% + \infirstlevel \sl + \global\advance\parencount by -1 +} + +\newcount\brackcount +\def\lbrb{% + \global\advance\brackcount by 1 + {\bf[}% +} +\def\rbrb{% + {\bf]}% + \global\advance\brackcount by -1 +} + +\def\checkparencounts{% + \ifnum\parencount=0 \else \badparencount \fi + \ifnum\brackcount=0 \else \badbrackcount \fi +} +% these should not use \errmessage; the glibc manual, at least, actually +% has such constructs (when documenting function pointers). +\def\badparencount{% + \message{Warning: unbalanced parentheses in @def...}% + \global\parencount=0 +} +\def\badbrackcount{% + \message{Warning: unbalanced square brackets in @def...}% + \global\brackcount=0 +} + + +\message{macros,} +% @macro. + +% To do this right we need a feature of e-TeX, \scantokens, +% which we arrange to emulate with a temporary file in ordinary TeX. +\ifx\eTeXversion\undefined + \newwrite\macscribble + \def\scantokens#1{% + \toks0={#1}% + \immediate\openout\macscribble=\jobname.tmp + \immediate\write\macscribble{\the\toks0}% + \immediate\closeout\macscribble + \input \jobname.tmp + } +\fi + +\def\scanmacro#1{% + \begingroup + \newlinechar`\^^M + \let\xeatspaces\eatspaces + % Undo catcode changes of \startcontents and \doprintindex + % When called from @insertcopying or (short)caption, we need active + % backslash to get it printed correctly. Previously, we had + % \catcode`\\=\other instead. We'll see whether a problem appears + % with macro expansion. --kasal, 19aug04 + \catcode`\@=0 \catcode`\\=\active \escapechar=`\@ + % ... and \example + \spaceisspace + % + % Append \endinput to make sure that TeX does not see the ending newline. + % I've verified that it is necessary both for e-TeX and for ordinary TeX + % --kasal, 29nov03 + \scantokens{#1\endinput}% + \endgroup +} + +\def\scanexp#1{% + \edef\temp{\noexpand\scanmacro{#1}}% + \temp +} + +\newcount\paramno % Count of parameters +\newtoks\macname % Macro name +\newif\ifrecursive % Is it recursive? + +% List of all defined macros in the form +% \definedummyword\macro1\definedummyword\macro2... +% Currently is also contains all @aliases; the list can be split +% if there is a need. +\def\macrolist{} + +% Add the macro to \macrolist +\def\addtomacrolist#1{\expandafter \addtomacrolistxxx \csname#1\endcsname} +\def\addtomacrolistxxx#1{% + \toks0 = \expandafter{\macrolist\definedummyword#1}% + \xdef\macrolist{\the\toks0}% +} + +% Utility routines. +% This does \let #1 = #2, with \csnames; that is, +% \let \csname#1\endcsname = \csname#2\endcsname +% (except of course we have to play expansion games). +% +\def\cslet#1#2{% + \expandafter\let + \csname#1\expandafter\endcsname + \csname#2\endcsname +} + +% Trim leading and trailing spaces off a string. +% Concepts from aro-bend problem 15 (see CTAN). +{\catcode`\@=11 +\gdef\eatspaces #1{\expandafter\trim@\expandafter{#1 }} +\gdef\trim@ #1{\trim@@ @#1 @ #1 @ @@} +\gdef\trim@@ #1@ #2@ #3@@{\trim@@@\empty #2 @} +\def\unbrace#1{#1} +\unbrace{\gdef\trim@@@ #1 } #2@{#1} +} + +% Trim a single trailing ^^M off a string. +{\catcode`\^^M=\other \catcode`\Q=3% +\gdef\eatcr #1{\eatcra #1Q^^MQ}% +\gdef\eatcra#1^^MQ{\eatcrb#1Q}% +\gdef\eatcrb#1Q#2Q{#1}% +} + +% Macro bodies are absorbed as an argument in a context where +% all characters are catcode 10, 11 or 12, except \ which is active +% (as in normal texinfo). It is necessary to change the definition of \. + +% Non-ASCII encodings make 8-bit characters active, so un-activate +% them to avoid their expansion. Must do this non-globally, to +% confine the change to the current group. + +% It's necessary to have hard CRs when the macro is executed. This is +% done by making ^^M (\endlinechar) catcode 12 when reading the macro +% body, and then making it the \newlinechar in \scanmacro. + +\def\scanctxt{% + \catcode`\"=\other + \catcode`\+=\other + \catcode`\<=\other + \catcode`\>=\other + \catcode`\@=\other + \catcode`\^=\other + \catcode`\_=\other + \catcode`\|=\other + \catcode`\~=\other + \ifx\declaredencoding\ascii \else \setnonasciicharscatcodenonglobal\other \fi +} + +\def\scanargctxt{% + \scanctxt + \catcode`\\=\other + \catcode`\^^M=\other +} + +\def\macrobodyctxt{% + \scanctxt + \catcode`\{=\other + \catcode`\}=\other + \catcode`\^^M=\other + \usembodybackslash +} + +\def\macroargctxt{% + \scanctxt + \catcode`\\=\other +} + +% \mbodybackslash is the definition of \ in @macro bodies. +% It maps \foo\ => \csname macarg.foo\endcsname => #N +% where N is the macro parameter number. +% We define \csname macarg.\endcsname to be \realbackslash, so +% \\ in macro replacement text gets you a backslash. + +{\catcode`@=0 @catcode`@\=@active + @gdef@usembodybackslash{@let\=@mbodybackslash} + @gdef@mbodybackslash#1\{@csname macarg.#1@endcsname} +} +\expandafter\def\csname macarg.\endcsname{\realbackslash} + +\def\macro{\recursivefalse\parsearg\macroxxx} +\def\rmacro{\recursivetrue\parsearg\macroxxx} + +\def\macroxxx#1{% + \getargs{#1}% now \macname is the macname and \argl the arglist + \ifx\argl\empty % no arguments + \paramno=0% + \else + \expandafter\parsemargdef \argl;% + \fi + \if1\csname ismacro.\the\macname\endcsname + \message{Warning: redefining \the\macname}% + \else + \expandafter\ifx\csname \the\macname\endcsname \relax + \else \errmessage{Macro name \the\macname\space already defined}\fi + \global\cslet{macsave.\the\macname}{\the\macname}% + \global\expandafter\let\csname ismacro.\the\macname\endcsname=1% + \addtomacrolist{\the\macname}% + \fi + \begingroup \macrobodyctxt + \ifrecursive \expandafter\parsermacbody + \else \expandafter\parsemacbody + \fi} + +\parseargdef\unmacro{% + \if1\csname ismacro.#1\endcsname + \global\cslet{#1}{macsave.#1}% + \global\expandafter\let \csname ismacro.#1\endcsname=0% + % Remove the macro name from \macrolist: + \begingroup + \expandafter\let\csname#1\endcsname \relax + \let\definedummyword\unmacrodo + \xdef\macrolist{\macrolist}% + \endgroup + \else + \errmessage{Macro #1 not defined}% + \fi +} + +% Called by \do from \dounmacro on each macro. The idea is to omit any +% macro definitions that have been changed to \relax. +% +\def\unmacrodo#1{% + \ifx #1\relax + % remove this + \else + \noexpand\definedummyword \noexpand#1% + \fi +} + +% This makes use of the obscure feature that if the last token of a +% <parameter list> is #, then the preceding argument is delimited by +% an opening brace, and that opening brace is not consumed. +\def\getargs#1{\getargsxxx#1{}} +\def\getargsxxx#1#{\getmacname #1 \relax\getmacargs} +\def\getmacname #1 #2\relax{\macname={#1}} +\def\getmacargs#1{\def\argl{#1}} + +% Parse the optional {params} list. Set up \paramno and \paramlist +% so \defmacro knows what to do. Define \macarg.blah for each blah +% in the params list, to be ##N where N is the position in that list. +% That gets used by \mbodybackslash (above). + +% We need to get `macro parameter char #' into several definitions. +% The technique used is stolen from LaTeX: let \hash be something +% unexpandable, insert that wherever you need a #, and then redefine +% it to # just before using the token list produced. +% +% The same technique is used to protect \eatspaces till just before +% the macro is used. + +\def\parsemargdef#1;{\paramno=0\def\paramlist{}% + \let\hash\relax\let\xeatspaces\relax\parsemargdefxxx#1,;,} +\def\parsemargdefxxx#1,{% + \if#1;\let\next=\relax + \else \let\next=\parsemargdefxxx + \advance\paramno by 1% + \expandafter\edef\csname macarg.\eatspaces{#1}\endcsname + {\xeatspaces{\hash\the\paramno}}% + \edef\paramlist{\paramlist\hash\the\paramno,}% + \fi\next} + +% These two commands read recursive and nonrecursive macro bodies. +% (They're different since rec and nonrec macros end differently.) + +\long\def\parsemacbody#1@end macro% +{\xdef\temp{\eatcr{#1}}\endgroup\defmacro}% +\long\def\parsermacbody#1@end rmacro% +{\xdef\temp{\eatcr{#1}}\endgroup\defmacro}% + +% This defines the macro itself. There are six cases: recursive and +% nonrecursive macros of zero, one, and many arguments. +% Much magic with \expandafter here. +% \xdef is used so that macro definitions will survive the file +% they're defined in; @include reads the file inside a group. +\def\defmacro{% + \let\hash=##% convert placeholders to macro parameter chars + \ifrecursive + \ifcase\paramno + % 0 + \expandafter\xdef\csname\the\macname\endcsname{% + \noexpand\scanmacro{\temp}}% + \or % 1 + \expandafter\xdef\csname\the\macname\endcsname{% + \bgroup\noexpand\macroargctxt + \noexpand\braceorline + \expandafter\noexpand\csname\the\macname xxx\endcsname}% + \expandafter\xdef\csname\the\macname xxx\endcsname##1{% + \egroup\noexpand\scanmacro{\temp}}% + \else % many + \expandafter\xdef\csname\the\macname\endcsname{% + \bgroup\noexpand\macroargctxt + \noexpand\csname\the\macname xx\endcsname}% + \expandafter\xdef\csname\the\macname xx\endcsname##1{% + \expandafter\noexpand\csname\the\macname xxx\endcsname ##1,}% + \expandafter\expandafter + \expandafter\xdef + \expandafter\expandafter + \csname\the\macname xxx\endcsname + \paramlist{\egroup\noexpand\scanmacro{\temp}}% + \fi + \else + \ifcase\paramno + % 0 + \expandafter\xdef\csname\the\macname\endcsname{% + \noexpand\norecurse{\the\macname}% + \noexpand\scanmacro{\temp}\egroup}% + \or % 1 + \expandafter\xdef\csname\the\macname\endcsname{% + \bgroup\noexpand\macroargctxt + \noexpand\braceorline + \expandafter\noexpand\csname\the\macname xxx\endcsname}% + \expandafter\xdef\csname\the\macname xxx\endcsname##1{% + \egroup + \noexpand\norecurse{\the\macname}% + \noexpand\scanmacro{\temp}\egroup}% + \else % many + \expandafter\xdef\csname\the\macname\endcsname{% + \bgroup\noexpand\macroargctxt + \expandafter\noexpand\csname\the\macname xx\endcsname}% + \expandafter\xdef\csname\the\macname xx\endcsname##1{% + \expandafter\noexpand\csname\the\macname xxx\endcsname ##1,}% + \expandafter\expandafter + \expandafter\xdef + \expandafter\expandafter + \csname\the\macname xxx\endcsname + \paramlist{% + \egroup + \noexpand\norecurse{\the\macname}% + \noexpand\scanmacro{\temp}\egroup}% + \fi + \fi} + +\def\norecurse#1{\bgroup\cslet{#1}{macsave.#1}} + +% \braceorline decides whether the next nonwhitespace character is a +% {. If so it reads up to the closing }, if not, it reads the whole +% line. Whatever was read is then fed to the next control sequence +% as an argument (by \parsebrace or \parsearg) +\def\braceorline#1{\let\macnamexxx=#1\futurelet\nchar\braceorlinexxx} +\def\braceorlinexxx{% + \ifx\nchar\bgroup\else + \expandafter\parsearg + \fi \macnamexxx} + + +% @alias. +% We need some trickery to remove the optional spaces around the equal +% sign. Just make them active and then expand them all to nothing. +\def\alias{\parseargusing\obeyspaces\aliasxxx} +\def\aliasxxx #1{\aliasyyy#1\relax} +\def\aliasyyy #1=#2\relax{% + {% + \expandafter\let\obeyedspace=\empty + \addtomacrolist{#1}% + \xdef\next{\global\let\makecsname{#1}=\makecsname{#2}}% + }% + \next +} + + +\message{cross references,} + +\newwrite\auxfile +\newif\ifhavexrefs % True if xref values are known. +\newif\ifwarnedxrefs % True if we warned once that they aren't known. + +% @inforef is relatively simple. +\def\inforef #1{\inforefzzz #1,,,,**} +\def\inforefzzz #1,#2,#3,#4**{\putwordSee{} \putwordInfo{} \putwordfile{} \file{\ignorespaces #3{}}, + node \samp{\ignorespaces#1{}}} + +% @node's only job in TeX is to define \lastnode, which is used in +% cross-references. The @node line might or might not have commas, and +% might or might not have spaces before the first comma, like: +% @node foo , bar , ... +% We don't want such trailing spaces in the node name. +% +\parseargdef\node{\checkenv{}\donode #1 ,\finishnodeparse} +% +% also remove a trailing comma, in case of something like this: +% @node Help-Cross, , , Cross-refs +\def\donode#1 ,#2\finishnodeparse{\dodonode #1,\finishnodeparse} +\def\dodonode#1,#2\finishnodeparse{\gdef\lastnode{#1}} + +\let\nwnode=\node +\let\lastnode=\empty + +% Write a cross-reference definition for the current node. #1 is the +% type (Ynumbered, Yappendix, Ynothing). +% +\def\donoderef#1{% + \ifx\lastnode\empty\else + \setref{\lastnode}{#1}% + \global\let\lastnode=\empty + \fi +} + +% @anchor{NAME} -- define xref target at arbitrary point. +% +\newcount\savesfregister +% +\def\savesf{\relax \ifhmode \savesfregister=\spacefactor \fi} +\def\restoresf{\relax \ifhmode \spacefactor=\savesfregister \fi} +\def\anchor#1{\savesf \setref{#1}{Ynothing}\restoresf \ignorespaces} + +% \setref{NAME}{SNT} defines a cross-reference point NAME (a node or an +% anchor), which consists of three parts: +% 1) NAME-title - the current sectioning name taken from \lastsection, +% or the anchor name. +% 2) NAME-snt - section number and type, passed as the SNT arg, or +% empty for anchors. +% 3) NAME-pg - the page number. +% +% This is called from \donoderef, \anchor, and \dofloat. In the case of +% floats, there is an additional part, which is not written here: +% 4) NAME-lof - the text as it should appear in a @listoffloats. +% +\def\setref#1#2{% + \pdfmkdest{#1}% + \iflinks + {% + \atdummies % preserve commands, but don't expand them + \edef\writexrdef##1##2{% + \write\auxfile{@xrdef{#1-% #1 of \setref, expanded by the \edef + ##1}{##2}}% these are parameters of \writexrdef + }% + \toks0 = \expandafter{\lastsection}% + \immediate \writexrdef{title}{\the\toks0 }% + \immediate \writexrdef{snt}{\csname #2\endcsname}% \Ynumbered etc. + \safewhatsit{\writexrdef{pg}{\folio}}% will be written later, during \shipout + }% + \fi +} + +% @xref, @pxref, and @ref generate cross-references. For \xrefX, #1 is +% the node name, #2 the name of the Info cross-reference, #3 the printed +% node name, #4 the name of the Info file, #5 the name of the printed +% manual. All but the node name can be omitted. +% +\def\pxref#1{\putwordsee{} \xrefX[#1,,,,,,,]} +\def\xref#1{\putwordSee{} \xrefX[#1,,,,,,,]} +\def\ref#1{\xrefX[#1,,,,,,,]} +\def\xrefX[#1,#2,#3,#4,#5,#6]{\begingroup + \unsepspaces + \def\printedmanual{\ignorespaces #5}% + \def\printedrefname{\ignorespaces #3}% + \setbox1=\hbox{\printedmanual\unskip}% + \setbox0=\hbox{\printedrefname\unskip}% + \ifdim \wd0 = 0pt + % No printed node name was explicitly given. + \expandafter\ifx\csname SETxref-automatic-section-title\endcsname\relax + % Use the node name inside the square brackets. + \def\printedrefname{\ignorespaces #1}% + \else + % Use the actual chapter/section title appear inside + % the square brackets. Use the real section title if we have it. + \ifdim \wd1 > 0pt + % It is in another manual, so we don't have it. + \def\printedrefname{\ignorespaces #1}% + \else + \ifhavexrefs + % We know the real title if we have the xref values. + \def\printedrefname{\refx{#1-title}{}}% + \else + % Otherwise just copy the Info node name. + \def\printedrefname{\ignorespaces #1}% + \fi% + \fi + \fi + \fi + % + % Make link in pdf output. + \ifpdf + \leavevmode + \getfilename{#4}% + {\indexnofonts + \turnoffactive + % See comments at \activebackslashdouble. + {\activebackslashdouble \xdef\pdfxrefdest{#1}% + \backslashparens\pdfxrefdest}% + % + \ifnum\filenamelength>0 + \startlink attr{/Border [0 0 0]}% + goto file{\the\filename.pdf} name{\pdfxrefdest}% + \else + \startlink attr{/Border [0 0 0]}% + goto name{\pdfmkpgn{\pdfxrefdest}}% + \fi + }% + \setcolor{\linkcolor}% + \fi + % + % Float references are printed completely differently: "Figure 1.2" + % instead of "[somenode], p.3". We distinguish them by the + % LABEL-title being set to a magic string. + {% + % Have to otherify everything special to allow the \csname to + % include an _ in the xref name, etc. + \indexnofonts + \turnoffactive + \expandafter\global\expandafter\let\expandafter\Xthisreftitle + \csname XR#1-title\endcsname + }% + \iffloat\Xthisreftitle + % If the user specified the print name (third arg) to the ref, + % print it instead of our usual "Figure 1.2". + \ifdim\wd0 = 0pt + \refx{#1-snt}{}% + \else + \printedrefname + \fi + % + % if the user also gave the printed manual name (fifth arg), append + % "in MANUALNAME". + \ifdim \wd1 > 0pt + \space \putwordin{} \cite{\printedmanual}% + \fi + \else + % node/anchor (non-float) references. + % + % If we use \unhbox0 and \unhbox1 to print the node names, TeX does not + % insert empty discretionaries after hyphens, which means that it will + % not find a line break at a hyphen in a node names. Since some manuals + % are best written with fairly long node names, containing hyphens, this + % is a loss. Therefore, we give the text of the node name again, so it + % is as if TeX is seeing it for the first time. + \ifdim \wd1 > 0pt + \putwordSection{} ``\printedrefname'' \putwordin{} \cite{\printedmanual}% + \else + % _ (for example) has to be the character _ for the purposes of the + % control sequence corresponding to the node, but it has to expand + % into the usual \leavevmode...\vrule stuff for purposes of + % printing. So we \turnoffactive for the \refx-snt, back on for the + % printing, back off for the \refx-pg. + {\turnoffactive + % Only output a following space if the -snt ref is nonempty; for + % @unnumbered and @anchor, it won't be. + \setbox2 = \hbox{\ignorespaces \refx{#1-snt}{}}% + \ifdim \wd2 > 0pt \refx{#1-snt}\space\fi + }% + % output the `[mynode]' via a macro so it can be overridden. + \xrefprintnodename\printedrefname + % + % But we always want a comma and a space: + ,\space + % + % output the `page 3'. + \turnoffactive \putwordpage\tie\refx{#1-pg}{}% + \fi + \fi + \endlink +\endgroup} + +% This macro is called from \xrefX for the `[nodename]' part of xref +% output. It's a separate macro only so it can be changed more easily, +% since square brackets don't work well in some documents. Particularly +% one that Bob is working on :). +% +\def\xrefprintnodename#1{[#1]} + +% Things referred to by \setref. +% +\def\Ynothing{} +\def\Yomitfromtoc{} +\def\Ynumbered{% + \ifnum\secno=0 + \putwordChapter@tie \the\chapno + \else \ifnum\subsecno=0 + \putwordSection@tie \the\chapno.\the\secno + \else \ifnum\subsubsecno=0 + \putwordSection@tie \the\chapno.\the\secno.\the\subsecno + \else + \putwordSection@tie \the\chapno.\the\secno.\the\subsecno.\the\subsubsecno + \fi\fi\fi +} +\def\Yappendix{% + \ifnum\secno=0 + \putwordAppendix@tie @char\the\appendixno{}% + \else \ifnum\subsecno=0 + \putwordSection@tie @char\the\appendixno.\the\secno + \else \ifnum\subsubsecno=0 + \putwordSection@tie @char\the\appendixno.\the\secno.\the\subsecno + \else + \putwordSection@tie + @char\the\appendixno.\the\secno.\the\subsecno.\the\subsubsecno + \fi\fi\fi +} + +% Define \refx{NAME}{SUFFIX} to reference a cross-reference string named NAME. +% If its value is nonempty, SUFFIX is output afterward. +% +\def\refx#1#2{% + {% + \indexnofonts + \otherbackslash + \expandafter\global\expandafter\let\expandafter\thisrefX + \csname XR#1\endcsname + }% + \ifx\thisrefX\relax + % If not defined, say something at least. + \angleleft un\-de\-fined\angleright + \iflinks + \ifhavexrefs + \message{\linenumber Undefined cross reference `#1'.}% + \else + \ifwarnedxrefs\else + \global\warnedxrefstrue + \message{Cross reference values unknown; you must run TeX again.}% + \fi + \fi + \fi + \else + % It's defined, so just use it. + \thisrefX + \fi + #2% Output the suffix in any case. +} + +% This is the macro invoked by entries in the aux file. Usually it's +% just a \def (we prepend XR to the control sequence name to avoid +% collisions). But if this is a float type, we have more work to do. +% +\def\xrdef#1#2{% + {% The node name might contain 8-bit characters, which in our current + % implementation are changed to commands like @'e. Don't let these + % mess up the control sequence name. + \indexnofonts + \turnoffactive + \xdef\safexrefname{#1}% + }% + % + \expandafter\gdef\csname XR\safexrefname\endcsname{#2}% remember this xref + % + % Was that xref control sequence that we just defined for a float? + \expandafter\iffloat\csname XR\safexrefname\endcsname + % it was a float, and we have the (safe) float type in \iffloattype. + \expandafter\let\expandafter\floatlist + \csname floatlist\iffloattype\endcsname + % + % Is this the first time we've seen this float type? + \expandafter\ifx\floatlist\relax + \toks0 = {\do}% yes, so just \do + \else + % had it before, so preserve previous elements in list. + \toks0 = \expandafter{\floatlist\do}% + \fi + % + % Remember this xref in the control sequence \floatlistFLOATTYPE, + % for later use in \listoffloats. + \expandafter\xdef\csname floatlist\iffloattype\endcsname{\the\toks0 + {\safexrefname}}% + \fi +} + +% Read the last existing aux file, if any. No error if none exists. +% +\def\tryauxfile{% + \openin 1 \jobname.aux + \ifeof 1 \else + \readdatafile{aux}% + \global\havexrefstrue + \fi + \closein 1 +} + +\def\setupdatafile{% + \catcode`\^^@=\other + \catcode`\^^A=\other + \catcode`\^^B=\other + \catcode`\^^C=\other + \catcode`\^^D=\other + \catcode`\^^E=\other + \catcode`\^^F=\other + \catcode`\^^G=\other + \catcode`\^^H=\other + \catcode`\^^K=\other + \catcode`\^^L=\other + \catcode`\^^N=\other + \catcode`\^^P=\other + \catcode`\^^Q=\other + \catcode`\^^R=\other + \catcode`\^^S=\other + \catcode`\^^T=\other + \catcode`\^^U=\other + \catcode`\^^V=\other + \catcode`\^^W=\other + \catcode`\^^X=\other + \catcode`\^^Z=\other + \catcode`\^^[=\other + \catcode`\^^\=\other + \catcode`\^^]=\other + \catcode`\^^^=\other + \catcode`\^^_=\other + % It was suggested to set the catcode of ^ to 7, which would allow ^^e4 etc. + % in xref tags, i.e., node names. But since ^^e4 notation isn't + % supported in the main text, it doesn't seem desirable. Furthermore, + % that is not enough: for node names that actually contain a ^ + % character, we would end up writing a line like this: 'xrdef {'hat + % b-title}{'hat b} and \xrdef does a \csname...\endcsname on the first + % argument, and \hat is not an expandable control sequence. It could + % all be worked out, but why? Either we support ^^ or we don't. + % + % The other change necessary for this was to define \auxhat: + % \def\auxhat{\def^{'hat }}% extra space so ok if followed by letter + % and then to call \auxhat in \setq. + % + \catcode`\^=\other + % + % Special characters. Should be turned off anyway, but... + \catcode`\~=\other + \catcode`\[=\other + \catcode`\]=\other + \catcode`\"=\other + \catcode`\_=\other + \catcode`\|=\other + \catcode`\<=\other + \catcode`\>=\other + \catcode`\$=\other + \catcode`\#=\other + \catcode`\&=\other + \catcode`\%=\other + \catcode`+=\other % avoid \+ for paranoia even though we've turned it off + % + % This is to support \ in node names and titles, since the \ + % characters end up in a \csname. It's easier than + % leaving it active and making its active definition an actual \ + % character. What I don't understand is why it works in the *value* + % of the xrdef. Seems like it should be a catcode12 \, and that + % should not typeset properly. But it works, so I'm moving on for + % now. --karl, 15jan04. + \catcode`\\=\other + % + % Make the characters 128-255 be printing characters. + {% + \count1=128 + \def\loop{% + \catcode\count1=\other + \advance\count1 by 1 + \ifnum \count1<256 \loop \fi + }% + }% + % + % @ is our escape character in .aux files, and we need braces. + \catcode`\{=1 + \catcode`\}=2 + \catcode`\@=0 +} + +\def\readdatafile#1{% +\begingroup + \setupdatafile + \input\jobname.#1 +\endgroup} + + +\message{insertions,} +% including footnotes. + +\newcount \footnoteno + +% The trailing space in the following definition for supereject is +% vital for proper filling; pages come out unaligned when you do a +% pagealignmacro call if that space before the closing brace is +% removed. (Generally, numeric constants should always be followed by a +% space to prevent strange expansion errors.) +\def\supereject{\par\penalty -20000\footnoteno =0 } + +% @footnotestyle is meaningful for info output only. +\let\footnotestyle=\comment + +{\catcode `\@=11 +% +% Auto-number footnotes. Otherwise like plain. +\gdef\footnote{% + \let\indent=\ptexindent + \let\noindent=\ptexnoindent + \global\advance\footnoteno by \@ne + \edef\thisfootno{$^{\the\footnoteno}$}% + % + % In case the footnote comes at the end of a sentence, preserve the + % extra spacing after we do the footnote number. + \let\@sf\empty + \ifhmode\edef\@sf{\spacefactor\the\spacefactor}\ptexslash\fi + % + % Remove inadvertent blank space before typesetting the footnote number. + \unskip + \thisfootno\@sf + \dofootnote +}% + +% Don't bother with the trickery in plain.tex to not require the +% footnote text as a parameter. Our footnotes don't need to be so general. +% +% Oh yes, they do; otherwise, @ifset (and anything else that uses +% \parseargline) fails inside footnotes because the tokens are fixed when +% the footnote is read. --karl, 16nov96. +% +\gdef\dofootnote{% + \insert\footins\bgroup + % We want to typeset this text as a normal paragraph, even if the + % footnote reference occurs in (for example) a display environment. + % So reset some parameters. + \hsize=\pagewidth + \interlinepenalty\interfootnotelinepenalty + \splittopskip\ht\strutbox % top baseline for broken footnotes + \splitmaxdepth\dp\strutbox + \floatingpenalty\@MM + \leftskip\z@skip + \rightskip\z@skip + \spaceskip\z@skip + \xspaceskip\z@skip + \parindent\defaultparindent + % + \smallfonts \rm + % + % Because we use hanging indentation in footnotes, a @noindent appears + % to exdent this text, so make it be a no-op. makeinfo does not use + % hanging indentation so @noindent can still be needed within footnote + % text after an @example or the like (not that this is good style). + \let\noindent = \relax + % + % Hang the footnote text off the number. Use \everypar in case the + % footnote extends for more than one paragraph. + \everypar = {\hang}% + \textindent{\thisfootno}% + % + % Don't crash into the line above the footnote text. Since this + % expands into a box, it must come within the paragraph, lest it + % provide a place where TeX can split the footnote. + \footstrut + \futurelet\next\fo@t +} +}%end \catcode `\@=11 + +% In case a @footnote appears in a vbox, save the footnote text and create +% the real \insert just after the vbox finished. Otherwise, the insertion +% would be lost. +% Similarly, if a @footnote appears inside an alignment, save the footnote +% text to a box and make the \insert when a row of the table is finished. +% And the same can be done for other insert classes. --kasal, 16nov03. + +% Replace the \insert primitive by a cheating macro. +% Deeper inside, just make sure that the saved insertions are not spilled +% out prematurely. +% +\def\startsavinginserts{% + \ifx \insert\ptexinsert + \let\insert\saveinsert + \else + \let\checkinserts\relax + \fi +} + +% This \insert replacement works for both \insert\footins{foo} and +% \insert\footins\bgroup foo\egroup, but it doesn't work for \insert27{foo}. +% +\def\saveinsert#1{% + \edef\next{\noexpand\savetobox \makeSAVEname#1}% + \afterassignment\next + % swallow the left brace + \let\temp = +} +\def\makeSAVEname#1{\makecsname{SAVE\expandafter\gobble\string#1}} +\def\savetobox#1{\global\setbox#1 = \vbox\bgroup \unvbox#1} + +\def\checksaveins#1{\ifvoid#1\else \placesaveins#1\fi} + +\def\placesaveins#1{% + \ptexinsert \csname\expandafter\gobblesave\string#1\endcsname + {\box#1}% +} + +% eat @SAVE -- beware, all of them have catcode \other: +{ + \def\dospecials{\do S\do A\do V\do E} \uncatcodespecials % ;-) + \gdef\gobblesave @SAVE{} +} + +% initialization: +\def\newsaveins #1{% + \edef\next{\noexpand\newsaveinsX \makeSAVEname#1}% + \next +} +\def\newsaveinsX #1{% + \csname newbox\endcsname #1% + \expandafter\def\expandafter\checkinserts\expandafter{\checkinserts + \checksaveins #1}% +} + +% initialize: +\let\checkinserts\empty +\newsaveins\footins +\newsaveins\margin + + +% @image. We use the macros from epsf.tex to support this. +% If epsf.tex is not installed and @image is used, we complain. +% +% Check for and read epsf.tex up front. If we read it only at @image +% time, we might be inside a group, and then its definitions would get +% undone and the next image would fail. +\openin 1 = epsf.tex +\ifeof 1 \else + % Do not bother showing banner with epsf.tex v2.7k (available in + % doc/epsf.tex and on ctan). + \def\epsfannounce{\toks0 = }% + \input epsf.tex +\fi +\closein 1 +% +% We will only complain once about lack of epsf.tex. +\newif\ifwarnednoepsf +\newhelp\noepsfhelp{epsf.tex must be installed for images to + work. It is also included in the Texinfo distribution, or you can get + it from ftp://tug.org/tex/epsf.tex.} +% +\def\image#1{% + \ifx\epsfbox\undefined + \ifwarnednoepsf \else + \errhelp = \noepsfhelp + \errmessage{epsf.tex not found, images will be ignored}% + \global\warnednoepsftrue + \fi + \else + \imagexxx #1,,,,,\finish + \fi +} +% +% Arguments to @image: +% #1 is (mandatory) image filename; we tack on .eps extension. +% #2 is (optional) width, #3 is (optional) height. +% #4 is (ignored optional) html alt text. +% #5 is (ignored optional) extension. +% #6 is just the usual extra ignored arg for parsing this stuff. +\newif\ifimagevmode +\def\imagexxx#1,#2,#3,#4,#5,#6\finish{\begingroup + \catcode`\^^M = 5 % in case we're inside an example + \normalturnoffactive % allow _ et al. in names + % If the image is by itself, center it. + \ifvmode + \imagevmodetrue + \nobreak\bigskip + % Usually we'll have text after the image which will insert + % \parskip glue, so insert it here too to equalize the space + % above and below. + \nobreak\vskip\parskip + \nobreak + \line\bgroup + \fi + % + % Output the image. + \ifpdf + \dopdfimage{#1}{#2}{#3}% + \else + % \epsfbox itself resets \epsf?size at each figure. + \setbox0 = \hbox{\ignorespaces #2}\ifdim\wd0 > 0pt \epsfxsize=#2\relax \fi + \setbox0 = \hbox{\ignorespaces #3}\ifdim\wd0 > 0pt \epsfysize=#3\relax \fi + \epsfbox{#1.eps}% + \fi + % + \ifimagevmode \egroup \bigbreak \fi % space after the image +\endgroup} + + +% @float FLOATTYPE,LABEL,LOC ... @end float for displayed figures, tables, +% etc. We don't actually implement floating yet, we always include the +% float "here". But it seemed the best name for the future. +% +\envparseargdef\float{\eatcommaspace\eatcommaspace\dofloat#1, , ,\finish} + +% There may be a space before second and/or third parameter; delete it. +\def\eatcommaspace#1, {#1,} + +% #1 is the optional FLOATTYPE, the text label for this float, typically +% "Figure", "Table", "Example", etc. Can't contain commas. If omitted, +% this float will not be numbered and cannot be referred to. +% +% #2 is the optional xref label. Also must be present for the float to +% be referable. +% +% #3 is the optional positioning argument; for now, it is ignored. It +% will somehow specify the positions allowed to float to (here, top, bottom). +% +% We keep a separate counter for each FLOATTYPE, which we reset at each +% chapter-level command. +\let\resetallfloatnos=\empty +% +\def\dofloat#1,#2,#3,#4\finish{% + \let\thiscaption=\empty + \let\thisshortcaption=\empty + % + % don't lose footnotes inside @float. + % + % BEWARE: when the floats start float, we have to issue warning whenever an + % insert appears inside a float which could possibly float. --kasal, 26may04 + % + \startsavinginserts + % + % We can't be used inside a paragraph. + \par + % + \vtop\bgroup + \def\floattype{#1}% + \def\floatlabel{#2}% + \def\floatloc{#3}% we do nothing with this yet. + % + \ifx\floattype\empty + \let\safefloattype=\empty + \else + {% + % the floattype might have accents or other special characters, + % but we need to use it in a control sequence name. + \indexnofonts + \turnoffactive + \xdef\safefloattype{\floattype}% + }% + \fi + % + % If label is given but no type, we handle that as the empty type. + \ifx\floatlabel\empty \else + % We want each FLOATTYPE to be numbered separately (Figure 1, + % Table 1, Figure 2, ...). (And if no label, no number.) + % + \expandafter\getfloatno\csname\safefloattype floatno\endcsname + \global\advance\floatno by 1 + % + {% + % This magic value for \lastsection is output by \setref as the + % XREFLABEL-title value. \xrefX uses it to distinguish float + % labels (which have a completely different output format) from + % node and anchor labels. And \xrdef uses it to construct the + % lists of floats. + % + \edef\lastsection{\floatmagic=\safefloattype}% + \setref{\floatlabel}{Yfloat}% + }% + \fi + % + % start with \parskip glue, I guess. + \vskip\parskip + % + % Don't suppress indentation if a float happens to start a section. + \restorefirstparagraphindent +} + +% we have these possibilities: +% @float Foo,lbl & @caption{Cap}: Foo 1.1: Cap +% @float Foo,lbl & no caption: Foo 1.1 +% @float Foo & @caption{Cap}: Foo: Cap +% @float Foo & no caption: Foo +% @float ,lbl & Caption{Cap}: 1.1: Cap +% @float ,lbl & no caption: 1.1 +% @float & @caption{Cap}: Cap +% @float & no caption: +% +\def\Efloat{% + \let\floatident = \empty + % + % In all cases, if we have a float type, it comes first. + \ifx\floattype\empty \else \def\floatident{\floattype}\fi + % + % If we have an xref label, the number comes next. + \ifx\floatlabel\empty \else + \ifx\floattype\empty \else % if also had float type, need tie first. + \appendtomacro\floatident{\tie}% + \fi + % the number. + \appendtomacro\floatident{\chaplevelprefix\the\floatno}% + \fi + % + % Start the printed caption with what we've constructed in + % \floatident, but keep it separate; we need \floatident again. + \let\captionline = \floatident + % + \ifx\thiscaption\empty \else + \ifx\floatident\empty \else + \appendtomacro\captionline{: }% had ident, so need a colon between + \fi + % + % caption text. + \appendtomacro\captionline{\scanexp\thiscaption}% + \fi + % + % If we have anything to print, print it, with space before. + % Eventually this needs to become an \insert. + \ifx\captionline\empty \else + \vskip.5\parskip + \captionline + % + % Space below caption. + \vskip\parskip + \fi + % + % If have an xref label, write the list of floats info. Do this + % after the caption, to avoid chance of it being a breakpoint. + \ifx\floatlabel\empty \else + % Write the text that goes in the lof to the aux file as + % \floatlabel-lof. Besides \floatident, we include the short + % caption if specified, else the full caption if specified, else nothing. + {% + \atdummies + % + % since we read the caption text in the macro world, where ^^M + % is turned into a normal character, we have to scan it back, so + % we don't write the literal three characters "^^M" into the aux file. + \scanexp{% + \xdef\noexpand\gtemp{% + \ifx\thisshortcaption\empty + \thiscaption + \else + \thisshortcaption + \fi + }% + }% + \immediate\write\auxfile{@xrdef{\floatlabel-lof}{\floatident + \ifx\gtemp\empty \else : \gtemp \fi}}% + }% + \fi + \egroup % end of \vtop + % + % place the captured inserts + % + % BEWARE: when the floats start floating, we have to issue warning + % whenever an insert appears inside a float which could possibly + % float. --kasal, 26may04 + % + \checkinserts +} + +% Append the tokens #2 to the definition of macro #1, not expanding either. +% +\def\appendtomacro#1#2{% + \expandafter\def\expandafter#1\expandafter{#1#2}% +} + +% @caption, @shortcaption +% +\def\caption{\docaption\thiscaption} +\def\shortcaption{\docaption\thisshortcaption} +\def\docaption{\checkenv\float \bgroup\scanargctxt\defcaption} +\def\defcaption#1#2{\egroup \def#1{#2}} + +% The parameter is the control sequence identifying the counter we are +% going to use. Create it if it doesn't exist and assign it to \floatno. +\def\getfloatno#1{% + \ifx#1\relax + % Haven't seen this figure type before. + \csname newcount\endcsname #1% + % + % Remember to reset this floatno at the next chap. + \expandafter\gdef\expandafter\resetallfloatnos + \expandafter{\resetallfloatnos #1=0 }% + \fi + \let\floatno#1% +} + +% \setref calls this to get the XREFLABEL-snt value. We want an @xref +% to the FLOATLABEL to expand to "Figure 3.1". We call \setref when we +% first read the @float command. +% +\def\Yfloat{\floattype@tie \chaplevelprefix\the\floatno}% + +% Magic string used for the XREFLABEL-title value, so \xrefX can +% distinguish floats from other xref types. +\def\floatmagic{!!float!!} + +% #1 is the control sequence we are passed; we expand into a conditional +% which is true if #1 represents a float ref. That is, the magic +% \lastsection value which we \setref above. +% +\def\iffloat#1{\expandafter\doiffloat#1==\finish} +% +% #1 is (maybe) the \floatmagic string. If so, #2 will be the +% (safe) float type for this float. We set \iffloattype to #2. +% +\def\doiffloat#1=#2=#3\finish{% + \def\temp{#1}% + \def\iffloattype{#2}% + \ifx\temp\floatmagic +} + +% @listoffloats FLOATTYPE - print a list of floats like a table of contents. +% +\parseargdef\listoffloats{% + \def\floattype{#1}% floattype + {% + % the floattype might have accents or other special characters, + % but we need to use it in a control sequence name. + \indexnofonts + \turnoffactive + \xdef\safefloattype{\floattype}% + }% + % + % \xrdef saves the floats as a \do-list in \floatlistSAFEFLOATTYPE. + \expandafter\ifx\csname floatlist\safefloattype\endcsname \relax + \ifhavexrefs + % if the user said @listoffloats foo but never @float foo. + \message{\linenumber No `\safefloattype' floats to list.}% + \fi + \else + \begingroup + \leftskip=\tocindent % indent these entries like a toc + \let\do=\listoffloatsdo + \csname floatlist\safefloattype\endcsname + \endgroup + \fi +} + +% This is called on each entry in a list of floats. We're passed the +% xref label, in the form LABEL-title, which is how we save it in the +% aux file. We strip off the -title and look up \XRLABEL-lof, which +% has the text we're supposed to typeset here. +% +% Figures without xref labels will not be included in the list (since +% they won't appear in the aux file). +% +\def\listoffloatsdo#1{\listoffloatsdoentry#1\finish} +\def\listoffloatsdoentry#1-title\finish{{% + % Can't fully expand XR#1-lof because it can contain anything. Just + % pass the control sequence. On the other hand, XR#1-pg is just the + % page number, and we want to fully expand that so we can get a link + % in pdf output. + \toksA = \expandafter{\csname XR#1-lof\endcsname}% + % + % use the same \entry macro we use to generate the TOC and index. + \edef\writeentry{\noexpand\entry{\the\toksA}{\csname XR#1-pg\endcsname}}% + \writeentry +}} + + +\message{localization,} + +% @documentlanguage is usually given very early, just after +% @setfilename. If done too late, it may not override everything +% properly. Single argument is the language (de) or locale (de_DE) +% abbreviation. It would be nice if we could set up a hyphenation file. +% +{ + \catcode`\_ = \active + \globaldefs=1 +\parseargdef\documentlanguage{\begingroup + \let_=\normalunderscore % normal _ character for filenames + \tex % read txi-??.tex file in plain TeX. + % Read the file by the name they passed if it exists. + \openin 1 txi-#1.tex + \ifeof 1 + \documentlanguagetrywithoutunderscore{#1_\finish}% + \else + \input txi-#1.tex + \fi + \closein 1 + \endgroup +\endgroup} +} +% +% If they passed de_DE, and txi-de_DE.tex doesn't exist, +% try txi-de.tex. +% +\def\documentlanguagetrywithoutunderscore#1_#2\finish{% + \openin 1 txi-#1.tex + \ifeof 1 + \errhelp = \nolanghelp + \errmessage{Cannot read language file txi-#1.tex}% + \else + \input txi-#1.tex + \fi + \closein 1 +} +% +\newhelp\nolanghelp{The given language definition file cannot be found or +is empty. Maybe you need to install it? In the current directory +should work if nowhere else does.} + +% Set the catcode of characters 128 through 255 to the specified number. +% +\def\setnonasciicharscatcode#1{% + \count255=128 + \loop\ifnum\count255<256 + \global\catcode\count255=#1\relax + \advance\count255 by 1 + \repeat +} + +\def\setnonasciicharscatcodenonglobal#1{% + \count255=128 + \loop\ifnum\count255<256 + \catcode\count255=#1\relax + \advance\count255 by 1 + \repeat +} + +% @documentencoding sets the definition of non-ASCII characters +% according to the specified encoding. +% +\parseargdef\documentencoding{% + % Encoding being declared for the document. + \def\declaredencoding{\csname #1.enc\endcsname}% + % + % Supported encodings: names converted to tokens in order to be able + % to compare them with \ifx. + \def\ascii{\csname US-ASCII.enc\endcsname}% + \def\latnine{\csname ISO-8859-15.enc\endcsname}% + \def\latone{\csname ISO-8859-1.enc\endcsname}% + \def\lattwo{\csname ISO-8859-2.enc\endcsname}% + \def\utfeight{\csname UTF-8.enc\endcsname}% + % + \ifx \declaredencoding \ascii + \asciichardefs + % + \else \ifx \declaredencoding \lattwo + \setnonasciicharscatcode\active + \lattwochardefs + % + \else \ifx \declaredencoding \latone + \setnonasciicharscatcode\active + \latonechardefs + % + \else \ifx \declaredencoding \latnine + \setnonasciicharscatcode\active + \latninechardefs + % + \else \ifx \declaredencoding \utfeight + \setnonasciicharscatcode\active + \utfeightchardefs + % + \else + \message{Unknown document encoding #1, ignoring.}% + % + \fi % utfeight + \fi % latnine + \fi % latone + \fi % lattwo + \fi % ascii +} + +% A message to be logged when using a character that isn't available +% the default font encoding (OT1). +% +\def\missingcharmsg#1{\message{Character missing in OT1 encoding: #1.}} + +% Take account of \c (plain) vs. \, (Texinfo) difference. +\def\cedilla#1{\ifx\c\ptexc\c{#1}\else\,{#1}\fi} + +% First, make active non-ASCII characters in order for them to be +% correctly categorized when TeX reads the replacement text of +% macros containing the character definitions. +\setnonasciicharscatcode\active +% +% Latin1 (ISO-8859-1) character definitions. +\def\latonechardefs{% + \gdef^^a0{~} + \gdef^^a1{\exclamdown} + \gdef^^a2{\missingcharmsg{CENT SIGN}} + \gdef^^a3{{\pounds}} + \gdef^^a4{\missingcharmsg{CURRENCY SIGN}} + \gdef^^a5{\missingcharmsg{YEN SIGN}} + \gdef^^a6{\missingcharmsg{BROKEN BAR}} + \gdef^^a7{\S} + \gdef^^a8{\"{}} + \gdef^^a9{\copyright} + \gdef^^aa{\ordf} + \gdef^^ab{\missingcharmsg{LEFT-POINTING DOUBLE ANGLE QUOTATION MARK}} + \gdef^^ac{$\lnot$} + \gdef^^ad{\-} + \gdef^^ae{\registeredsymbol} + \gdef^^af{\={}} + % + \gdef^^b0{\textdegree} + \gdef^^b1{$\pm$} + \gdef^^b2{$^2$} + \gdef^^b3{$^3$} + \gdef^^b4{\'{}} + \gdef^^b5{$\mu$} + \gdef^^b6{\P} + % + \gdef^^b7{$^.$} + \gdef^^b8{\cedilla\ } + \gdef^^b9{$^1$} + \gdef^^ba{\ordm} + % + \gdef^^bb{\missingcharmsg{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}} + \gdef^^bc{$1\over4$} + \gdef^^bd{$1\over2$} + \gdef^^be{$3\over4$} + \gdef^^bf{\questiondown} + % + \gdef^^c0{\`A} + \gdef^^c1{\'A} + \gdef^^c2{\^A} + \gdef^^c3{\~A} + \gdef^^c4{\"A} + \gdef^^c5{\ringaccent A} + \gdef^^c6{\AE} + \gdef^^c7{\cedilla C} + \gdef^^c8{\`E} + \gdef^^c9{\'E} + \gdef^^ca{\^E} + \gdef^^cb{\"E} + \gdef^^cc{\`I} + \gdef^^cd{\'I} + \gdef^^ce{\^I} + \gdef^^cf{\"I} + % + \gdef^^d0{\missingcharmsg{LATIN CAPITAL LETTER ETH}} + \gdef^^d1{\~N} + \gdef^^d2{\`O} + \gdef^^d3{\'O} + \gdef^^d4{\^O} + \gdef^^d5{\~O} + \gdef^^d6{\"O} + \gdef^^d7{$\times$} + \gdef^^d8{\O} + \gdef^^d9{\`U} + \gdef^^da{\'U} + \gdef^^db{\^U} + \gdef^^dc{\"U} + \gdef^^dd{\'Y} + \gdef^^de{\missingcharmsg{LATIN CAPITAL LETTER THORN}} + \gdef^^df{\ss} + % + \gdef^^e0{\`a} + \gdef^^e1{\'a} + \gdef^^e2{\^a} + \gdef^^e3{\~a} + \gdef^^e4{\"a} + \gdef^^e5{\ringaccent a} + \gdef^^e6{\ae} + \gdef^^e7{\cedilla c} + \gdef^^e8{\`e} + \gdef^^e9{\'e} + \gdef^^ea{\^e} + \gdef^^eb{\"e} + \gdef^^ec{\`{\dotless i}} + \gdef^^ed{\'{\dotless i}} + \gdef^^ee{\^{\dotless i}} + \gdef^^ef{\"{\dotless i}} + % + \gdef^^f0{\missingcharmsg{LATIN SMALL LETTER ETH}} + \gdef^^f1{\~n} + \gdef^^f2{\`o} + \gdef^^f3{\'o} + \gdef^^f4{\^o} + \gdef^^f5{\~o} + \gdef^^f6{\"o} + \gdef^^f7{$\div$} + \gdef^^f8{\o} + \gdef^^f9{\`u} + \gdef^^fa{\'u} + \gdef^^fb{\^u} + \gdef^^fc{\"u} + \gdef^^fd{\'y} + \gdef^^fe{\missingcharmsg{LATIN SMALL LETTER THORN}} + \gdef^^ff{\"y} +} + +% Latin9 (ISO-8859-15) encoding character definitions. +\def\latninechardefs{% + % Encoding is almost identical to Latin1. + \latonechardefs + % + \gdef^^a4{\euro} + \gdef^^a6{\v S} + \gdef^^a8{\v s} + \gdef^^b4{\v Z} + \gdef^^b8{\v z} + \gdef^^bc{\OE} + \gdef^^bd{\oe} + \gdef^^be{\"Y} +} + +% Latin2 (ISO-8859-2) character definitions. +\def\lattwochardefs{% + \gdef^^a0{~} + \gdef^^a1{\missingcharmsg{LATIN CAPITAL LETTER A WITH OGONEK}} + \gdef^^a2{\u{}} + \gdef^^a3{\L} + \gdef^^a4{\missingcharmsg{CURRENCY SIGN}} + \gdef^^a5{\v L} + \gdef^^a6{\'S} + \gdef^^a7{\S} + \gdef^^a8{\"{}} + \gdef^^a9{\v S} + \gdef^^aa{\cedilla S} + \gdef^^ab{\v T} + \gdef^^ac{\'Z} + \gdef^^ad{\-} + \gdef^^ae{\v Z} + \gdef^^af{\dotaccent Z} + % + \gdef^^b0{\textdegree} + \gdef^^b1{\missingcharmsg{LATIN SMALL LETTER A WITH OGONEK}} + \gdef^^b2{\missingcharmsg{OGONEK}} + \gdef^^b3{\l} + \gdef^^b4{\'{}} + \gdef^^b5{\v l} + \gdef^^b6{\'s} + \gdef^^b7{\v{}} + \gdef^^b8{\cedilla\ } + \gdef^^b9{\v s} + \gdef^^ba{\cedilla s} + \gdef^^bb{\v t} + \gdef^^bc{\'z} + \gdef^^bd{\H{}} + \gdef^^be{\v z} + \gdef^^bf{\dotaccent z} + % + \gdef^^c0{\'R} + \gdef^^c1{\'A} + \gdef^^c2{\^A} + \gdef^^c3{\u A} + \gdef^^c4{\"A} + \gdef^^c5{\'L} + \gdef^^c6{\'C} + \gdef^^c7{\cedilla C} + \gdef^^c8{\v C} + \gdef^^c9{\'E} + \gdef^^ca{\missingcharmsg{LATIN CAPITAL LETTER E WITH OGONEK}} + \gdef^^cb{\"E} + \gdef^^cc{\v E} + \gdef^^cd{\'I} + \gdef^^ce{\^I} + \gdef^^cf{\v D} + % + \gdef^^d0{\missingcharmsg{LATIN CAPITAL LETTER D WITH STROKE}} + \gdef^^d1{\'N} + \gdef^^d2{\v N} + \gdef^^d3{\'O} + \gdef^^d4{\^O} + \gdef^^d5{\H O} + \gdef^^d6{\"O} + \gdef^^d7{$\times$} + \gdef^^d8{\v R} + \gdef^^d9{\ringaccent U} + \gdef^^da{\'U} + \gdef^^db{\H U} + \gdef^^dc{\"U} + \gdef^^dd{\'Y} + \gdef^^de{\cedilla T} + \gdef^^df{\ss} + % + \gdef^^e0{\'r} + \gdef^^e1{\'a} + \gdef^^e2{\^a} + \gdef^^e3{\u a} + \gdef^^e4{\"a} + \gdef^^e5{\'l} + \gdef^^e6{\'c} + \gdef^^e7{\cedilla c} + \gdef^^e8{\v c} + \gdef^^e9{\'e} + \gdef^^ea{\missingcharmsg{LATIN SMALL LETTER E WITH OGONEK}} + \gdef^^eb{\"e} + \gdef^^ec{\v e} + \gdef^^ed{\'\i} + \gdef^^ee{\^\i} + \gdef^^ef{\v d} + % + \gdef^^f0{\missingcharmsg{LATIN SMALL LETTER D WITH STROKE}} + \gdef^^f1{\'n} + \gdef^^f2{\v n} + \gdef^^f3{\'o} + \gdef^^f4{\^o} + \gdef^^f5{\H o} + \gdef^^f6{\"o} + \gdef^^f7{$\div$} + \gdef^^f8{\v r} + \gdef^^f9{\ringaccent u} + \gdef^^fa{\'u} + \gdef^^fb{\H u} + \gdef^^fc{\"u} + \gdef^^fd{\'y} + \gdef^^fe{\cedilla t} + \gdef^^ff{\dotaccent{}} +} + +% UTF-8 character definitions. +% +% This code to support UTF-8 is based on LaTeX's utf8.def, with some +% changes for Texinfo conventions. It is included here under the GPL by +% permission from Frank Mittelbach and the LaTeX team. +% +\newcount\countUTFx +\newcount\countUTFy +\newcount\countUTFz + +\gdef\UTFviiiTwoOctets#1#2{\expandafter + \UTFviiiDefined\csname u8:#1\string #2\endcsname} +% +\gdef\UTFviiiThreeOctets#1#2#3{\expandafter + \UTFviiiDefined\csname u8:#1\string #2\string #3\endcsname} +% +\gdef\UTFviiiFourOctets#1#2#3#4{\expandafter + \UTFviiiDefined\csname u8:#1\string #2\string #3\string #4\endcsname} + +\gdef\UTFviiiDefined#1{% + \ifx #1\relax + \message{\linenumber Unicode char \string #1 not defined for Texinfo}% + \else + \expandafter #1% + \fi +} + +\begingroup + \catcode`\~13 + \catcode`\"12 + + \def\UTFviiiLoop{% + \global\catcode\countUTFx\active + \uccode`\~\countUTFx + \uppercase\expandafter{\UTFviiiTmp}% + \advance\countUTFx by 1 + \ifnum\countUTFx < \countUTFy + \expandafter\UTFviiiLoop + \fi} + + \countUTFx = "C2 + \countUTFy = "E0 + \def\UTFviiiTmp{% + \xdef~{\noexpand\UTFviiiTwoOctets\string~}} + \UTFviiiLoop + + \countUTFx = "E0 + \countUTFy = "F0 + \def\UTFviiiTmp{% + \xdef~{\noexpand\UTFviiiThreeOctets\string~}} + \UTFviiiLoop + + \countUTFx = "F0 + \countUTFy = "F4 + \def\UTFviiiTmp{% + \xdef~{\noexpand\UTFviiiFourOctets\string~}} + \UTFviiiLoop +\endgroup + +\begingroup + \catcode`\"=12 + \catcode`\<=12 + \catcode`\.=12 + \catcode`\,=12 + \catcode`\;=12 + \catcode`\!=12 + \catcode`\~=13 + + \gdef\DeclareUnicodeCharacter#1#2{% + \countUTFz = "#1\relax + \wlog{\space\space defining Unicode char U+#1 (decimal \the\countUTFz)}% + \begingroup + \parseXMLCharref + \def\UTFviiiTwoOctets##1##2{% + \csname u8:##1\string ##2\endcsname}% + \def\UTFviiiThreeOctets##1##2##3{% + \csname u8:##1\string ##2\string ##3\endcsname}% + \def\UTFviiiFourOctets##1##2##3##4{% + \csname u8:##1\string ##2\string ##3\string ##4\endcsname}% + \expandafter\expandafter\expandafter\expandafter + \expandafter\expandafter\expandafter + \gdef\UTFviiiTmp{#2}% + \endgroup} + + \gdef\parseXMLCharref{% + \ifnum\countUTFz < "A0\relax + \errhelp = \EMsimple + \errmessage{Cannot define Unicode char value < 00A0}% + \else\ifnum\countUTFz < "800\relax + \parseUTFviiiA,% + \parseUTFviiiB C\UTFviiiTwoOctets.,% + \else\ifnum\countUTFz < "10000\relax + \parseUTFviiiA;% + \parseUTFviiiA,% + \parseUTFviiiB E\UTFviiiThreeOctets.{,;}% + \else + \parseUTFviiiA;% + \parseUTFviiiA,% + \parseUTFviiiA!% + \parseUTFviiiB F\UTFviiiFourOctets.{!,;}% + \fi\fi\fi + } + + \gdef\parseUTFviiiA#1{% + \countUTFx = \countUTFz + \divide\countUTFz by 64 + \countUTFy = \countUTFz + \multiply\countUTFz by 64 + \advance\countUTFx by -\countUTFz + \advance\countUTFx by 128 + \uccode `#1\countUTFx + \countUTFz = \countUTFy} + + \gdef\parseUTFviiiB#1#2#3#4{% + \advance\countUTFz by "#10\relax + \uccode `#3\countUTFz + \uppercase{\gdef\UTFviiiTmp{#2#3#4}}} +\endgroup + +\def\utfeightchardefs{% + \DeclareUnicodeCharacter{00A0}{\tie} + \DeclareUnicodeCharacter{00A1}{\exclamdown} + \DeclareUnicodeCharacter{00A3}{\pounds} + \DeclareUnicodeCharacter{00A8}{\"{ }} + \DeclareUnicodeCharacter{00A9}{\copyright} + \DeclareUnicodeCharacter{00AA}{\ordf} + \DeclareUnicodeCharacter{00AB}{\guillemetleft} + \DeclareUnicodeCharacter{00AD}{\-} + \DeclareUnicodeCharacter{00AE}{\registeredsymbol} + \DeclareUnicodeCharacter{00AF}{\={ }} + + \DeclareUnicodeCharacter{00B0}{\ringaccent{ }} + \DeclareUnicodeCharacter{00B4}{\'{ }} + \DeclareUnicodeCharacter{00B8}{\cedilla{ }} + \DeclareUnicodeCharacter{00BA}{\ordm} + \DeclareUnicodeCharacter{00BB}{\guillemetright} + \DeclareUnicodeCharacter{00BF}{\questiondown} + + \DeclareUnicodeCharacter{00C0}{\`A} + \DeclareUnicodeCharacter{00C1}{\'A} + \DeclareUnicodeCharacter{00C2}{\^A} + \DeclareUnicodeCharacter{00C3}{\~A} + \DeclareUnicodeCharacter{00C4}{\"A} + \DeclareUnicodeCharacter{00C5}{\AA} + \DeclareUnicodeCharacter{00C6}{\AE} + \DeclareUnicodeCharacter{00C7}{\cedilla{C}} + \DeclareUnicodeCharacter{00C8}{\`E} + \DeclareUnicodeCharacter{00C9}{\'E} + \DeclareUnicodeCharacter{00CA}{\^E} + \DeclareUnicodeCharacter{00CB}{\"E} + \DeclareUnicodeCharacter{00CC}{\`I} + \DeclareUnicodeCharacter{00CD}{\'I} + \DeclareUnicodeCharacter{00CE}{\^I} + \DeclareUnicodeCharacter{00CF}{\"I} + + \DeclareUnicodeCharacter{00D1}{\~N} + \DeclareUnicodeCharacter{00D2}{\`O} + \DeclareUnicodeCharacter{00D3}{\'O} + \DeclareUnicodeCharacter{00D4}{\^O} + \DeclareUnicodeCharacter{00D5}{\~O} + \DeclareUnicodeCharacter{00D6}{\"O} + \DeclareUnicodeCharacter{00D8}{\O} + \DeclareUnicodeCharacter{00D9}{\`U} + \DeclareUnicodeCharacter{00DA}{\'U} + \DeclareUnicodeCharacter{00DB}{\^U} + \DeclareUnicodeCharacter{00DC}{\"U} + \DeclareUnicodeCharacter{00DD}{\'Y} + \DeclareUnicodeCharacter{00DF}{\ss} + + \DeclareUnicodeCharacter{00E0}{\`a} + \DeclareUnicodeCharacter{00E1}{\'a} + \DeclareUnicodeCharacter{00E2}{\^a} + \DeclareUnicodeCharacter{00E3}{\~a} + \DeclareUnicodeCharacter{00E4}{\"a} + \DeclareUnicodeCharacter{00E5}{\aa} + \DeclareUnicodeCharacter{00E6}{\ae} + \DeclareUnicodeCharacter{00E7}{\cedilla{c}} + \DeclareUnicodeCharacter{00E8}{\`e} + \DeclareUnicodeCharacter{00E9}{\'e} + \DeclareUnicodeCharacter{00EA}{\^e} + \DeclareUnicodeCharacter{00EB}{\"e} + \DeclareUnicodeCharacter{00EC}{\`{\dotless{i}}} + \DeclareUnicodeCharacter{00ED}{\'{\dotless{i}}} + \DeclareUnicodeCharacter{00EE}{\^{\dotless{i}}} + \DeclareUnicodeCharacter{00EF}{\"{\dotless{i}}} + + \DeclareUnicodeCharacter{00F1}{\~n} + \DeclareUnicodeCharacter{00F2}{\`o} + \DeclareUnicodeCharacter{00F3}{\'o} + \DeclareUnicodeCharacter{00F4}{\^o} + \DeclareUnicodeCharacter{00F5}{\~o} + \DeclareUnicodeCharacter{00F6}{\"o} + \DeclareUnicodeCharacter{00F8}{\o} + \DeclareUnicodeCharacter{00F9}{\`u} + \DeclareUnicodeCharacter{00FA}{\'u} + \DeclareUnicodeCharacter{00FB}{\^u} + \DeclareUnicodeCharacter{00FC}{\"u} + \DeclareUnicodeCharacter{00FD}{\'y} + \DeclareUnicodeCharacter{00FF}{\"y} + + \DeclareUnicodeCharacter{0100}{\=A} + \DeclareUnicodeCharacter{0101}{\=a} + \DeclareUnicodeCharacter{0102}{\u{A}} + \DeclareUnicodeCharacter{0103}{\u{a}} + \DeclareUnicodeCharacter{0106}{\'C} + \DeclareUnicodeCharacter{0107}{\'c} + \DeclareUnicodeCharacter{0108}{\^C} + \DeclareUnicodeCharacter{0109}{\^c} + \DeclareUnicodeCharacter{010A}{\dotaccent{C}} + \DeclareUnicodeCharacter{010B}{\dotaccent{c}} + \DeclareUnicodeCharacter{010C}{\v{C}} + \DeclareUnicodeCharacter{010D}{\v{c}} + \DeclareUnicodeCharacter{010E}{\v{D}} + + \DeclareUnicodeCharacter{0112}{\=E} + \DeclareUnicodeCharacter{0113}{\=e} + \DeclareUnicodeCharacter{0114}{\u{E}} + \DeclareUnicodeCharacter{0115}{\u{e}} + \DeclareUnicodeCharacter{0116}{\dotaccent{E}} + \DeclareUnicodeCharacter{0117}{\dotaccent{e}} + \DeclareUnicodeCharacter{011A}{\v{E}} + \DeclareUnicodeCharacter{011B}{\v{e}} + \DeclareUnicodeCharacter{011C}{\^G} + \DeclareUnicodeCharacter{011D}{\^g} + \DeclareUnicodeCharacter{011E}{\u{G}} + \DeclareUnicodeCharacter{011F}{\u{g}} + + \DeclareUnicodeCharacter{0120}{\dotaccent{G}} + \DeclareUnicodeCharacter{0121}{\dotaccent{g}} + \DeclareUnicodeCharacter{0124}{\^H} + \DeclareUnicodeCharacter{0125}{\^h} + \DeclareUnicodeCharacter{0128}{\~I} + \DeclareUnicodeCharacter{0129}{\~{\dotless{i}}} + \DeclareUnicodeCharacter{012A}{\=I} + \DeclareUnicodeCharacter{012B}{\={\dotless{i}}} + \DeclareUnicodeCharacter{012C}{\u{I}} + \DeclareUnicodeCharacter{012D}{\u{\dotless{i}}} + + \DeclareUnicodeCharacter{0130}{\dotaccent{I}} + \DeclareUnicodeCharacter{0131}{\dotless{i}} + \DeclareUnicodeCharacter{0132}{IJ} + \DeclareUnicodeCharacter{0133}{ij} + \DeclareUnicodeCharacter{0134}{\^J} + \DeclareUnicodeCharacter{0135}{\^{\dotless{j}}} + \DeclareUnicodeCharacter{0139}{\'L} + \DeclareUnicodeCharacter{013A}{\'l} + + \DeclareUnicodeCharacter{0141}{\L} + \DeclareUnicodeCharacter{0142}{\l} + \DeclareUnicodeCharacter{0143}{\'N} + \DeclareUnicodeCharacter{0144}{\'n} + \DeclareUnicodeCharacter{0147}{\v{N}} + \DeclareUnicodeCharacter{0148}{\v{n}} + \DeclareUnicodeCharacter{014C}{\=O} + \DeclareUnicodeCharacter{014D}{\=o} + \DeclareUnicodeCharacter{014E}{\u{O}} + \DeclareUnicodeCharacter{014F}{\u{o}} + + \DeclareUnicodeCharacter{0150}{\H{O}} + \DeclareUnicodeCharacter{0151}{\H{o}} + \DeclareUnicodeCharacter{0152}{\OE} + \DeclareUnicodeCharacter{0153}{\oe} + \DeclareUnicodeCharacter{0154}{\'R} + \DeclareUnicodeCharacter{0155}{\'r} + \DeclareUnicodeCharacter{0158}{\v{R}} + \DeclareUnicodeCharacter{0159}{\v{r}} + \DeclareUnicodeCharacter{015A}{\'S} + \DeclareUnicodeCharacter{015B}{\'s} + \DeclareUnicodeCharacter{015C}{\^S} + \DeclareUnicodeCharacter{015D}{\^s} + \DeclareUnicodeCharacter{015E}{\cedilla{S}} + \DeclareUnicodeCharacter{015F}{\cedilla{s}} + + \DeclareUnicodeCharacter{0160}{\v{S}} + \DeclareUnicodeCharacter{0161}{\v{s}} + \DeclareUnicodeCharacter{0162}{\cedilla{t}} + \DeclareUnicodeCharacter{0163}{\cedilla{T}} + \DeclareUnicodeCharacter{0164}{\v{T}} + + \DeclareUnicodeCharacter{0168}{\~U} + \DeclareUnicodeCharacter{0169}{\~u} + \DeclareUnicodeCharacter{016A}{\=U} + \DeclareUnicodeCharacter{016B}{\=u} + \DeclareUnicodeCharacter{016C}{\u{U}} + \DeclareUnicodeCharacter{016D}{\u{u}} + \DeclareUnicodeCharacter{016E}{\ringaccent{U}} + \DeclareUnicodeCharacter{016F}{\ringaccent{u}} + + \DeclareUnicodeCharacter{0170}{\H{U}} + \DeclareUnicodeCharacter{0171}{\H{u}} + \DeclareUnicodeCharacter{0174}{\^W} + \DeclareUnicodeCharacter{0175}{\^w} + \DeclareUnicodeCharacter{0176}{\^Y} + \DeclareUnicodeCharacter{0177}{\^y} + \DeclareUnicodeCharacter{0178}{\"Y} + \DeclareUnicodeCharacter{0179}{\'Z} + \DeclareUnicodeCharacter{017A}{\'z} + \DeclareUnicodeCharacter{017B}{\dotaccent{Z}} + \DeclareUnicodeCharacter{017C}{\dotaccent{z}} + \DeclareUnicodeCharacter{017D}{\v{Z}} + \DeclareUnicodeCharacter{017E}{\v{z}} + + \DeclareUnicodeCharacter{01C4}{D\v{Z}} + \DeclareUnicodeCharacter{01C5}{D\v{z}} + \DeclareUnicodeCharacter{01C6}{d\v{z}} + \DeclareUnicodeCharacter{01C7}{LJ} + \DeclareUnicodeCharacter{01C8}{Lj} + \DeclareUnicodeCharacter{01C9}{lj} + \DeclareUnicodeCharacter{01CA}{NJ} + \DeclareUnicodeCharacter{01CB}{Nj} + \DeclareUnicodeCharacter{01CC}{nj} + \DeclareUnicodeCharacter{01CD}{\v{A}} + \DeclareUnicodeCharacter{01CE}{\v{a}} + \DeclareUnicodeCharacter{01CF}{\v{I}} + + \DeclareUnicodeCharacter{01D0}{\v{\dotless{i}}} + \DeclareUnicodeCharacter{01D1}{\v{O}} + \DeclareUnicodeCharacter{01D2}{\v{o}} + \DeclareUnicodeCharacter{01D3}{\v{U}} + \DeclareUnicodeCharacter{01D4}{\v{u}} + + \DeclareUnicodeCharacter{01E2}{\={\AE}} + \DeclareUnicodeCharacter{01E3}{\={\ae}} + \DeclareUnicodeCharacter{01E6}{\v{G}} + \DeclareUnicodeCharacter{01E7}{\v{g}} + \DeclareUnicodeCharacter{01E8}{\v{K}} + \DeclareUnicodeCharacter{01E9}{\v{k}} + + \DeclareUnicodeCharacter{01F0}{\v{\dotless{j}}} + \DeclareUnicodeCharacter{01F1}{DZ} + \DeclareUnicodeCharacter{01F2}{Dz} + \DeclareUnicodeCharacter{01F3}{dz} + \DeclareUnicodeCharacter{01F4}{\'G} + \DeclareUnicodeCharacter{01F5}{\'g} + \DeclareUnicodeCharacter{01F8}{\`N} + \DeclareUnicodeCharacter{01F9}{\`n} + \DeclareUnicodeCharacter{01FC}{\'{\AE}} + \DeclareUnicodeCharacter{01FD}{\'{\ae}} + \DeclareUnicodeCharacter{01FE}{\'{\O}} + \DeclareUnicodeCharacter{01FF}{\'{\o}} + + \DeclareUnicodeCharacter{021E}{\v{H}} + \DeclareUnicodeCharacter{021F}{\v{h}} + + \DeclareUnicodeCharacter{0226}{\dotaccent{A}} + \DeclareUnicodeCharacter{0227}{\dotaccent{a}} + \DeclareUnicodeCharacter{0228}{\cedilla{E}} + \DeclareUnicodeCharacter{0229}{\cedilla{e}} + \DeclareUnicodeCharacter{022E}{\dotaccent{O}} + \DeclareUnicodeCharacter{022F}{\dotaccent{o}} + + \DeclareUnicodeCharacter{0232}{\=Y} + \DeclareUnicodeCharacter{0233}{\=y} + \DeclareUnicodeCharacter{0237}{\dotless{j}} + + \DeclareUnicodeCharacter{1E02}{\dotaccent{B}} + \DeclareUnicodeCharacter{1E03}{\dotaccent{b}} + \DeclareUnicodeCharacter{1E04}{\udotaccent{B}} + \DeclareUnicodeCharacter{1E05}{\udotaccent{b}} + \DeclareUnicodeCharacter{1E06}{\ubaraccent{B}} + \DeclareUnicodeCharacter{1E07}{\ubaraccent{b}} + \DeclareUnicodeCharacter{1E0A}{\dotaccent{D}} + \DeclareUnicodeCharacter{1E0B}{\dotaccent{d}} + \DeclareUnicodeCharacter{1E0C}{\udotaccent{D}} + \DeclareUnicodeCharacter{1E0D}{\udotaccent{d}} + \DeclareUnicodeCharacter{1E0E}{\ubaraccent{D}} + \DeclareUnicodeCharacter{1E0F}{\ubaraccent{d}} + + \DeclareUnicodeCharacter{1E1E}{\dotaccent{F}} + \DeclareUnicodeCharacter{1E1F}{\dotaccent{f}} + + \DeclareUnicodeCharacter{1E20}{\=G} + \DeclareUnicodeCharacter{1E21}{\=g} + \DeclareUnicodeCharacter{1E22}{\dotaccent{H}} + \DeclareUnicodeCharacter{1E23}{\dotaccent{h}} + \DeclareUnicodeCharacter{1E24}{\udotaccent{H}} + \DeclareUnicodeCharacter{1E25}{\udotaccent{h}} + \DeclareUnicodeCharacter{1E26}{\"H} + \DeclareUnicodeCharacter{1E27}{\"h} + + \DeclareUnicodeCharacter{1E30}{\'K} + \DeclareUnicodeCharacter{1E31}{\'k} + \DeclareUnicodeCharacter{1E32}{\udotaccent{K}} + \DeclareUnicodeCharacter{1E33}{\udotaccent{k}} + \DeclareUnicodeCharacter{1E34}{\ubaraccent{K}} + \DeclareUnicodeCharacter{1E35}{\ubaraccent{k}} + \DeclareUnicodeCharacter{1E36}{\udotaccent{L}} + \DeclareUnicodeCharacter{1E37}{\udotaccent{l}} + \DeclareUnicodeCharacter{1E3A}{\ubaraccent{L}} + \DeclareUnicodeCharacter{1E3B}{\ubaraccent{l}} + \DeclareUnicodeCharacter{1E3E}{\'M} + \DeclareUnicodeCharacter{1E3F}{\'m} + + \DeclareUnicodeCharacter{1E40}{\dotaccent{M}} + \DeclareUnicodeCharacter{1E41}{\dotaccent{m}} + \DeclareUnicodeCharacter{1E42}{\udotaccent{M}} + \DeclareUnicodeCharacter{1E43}{\udotaccent{m}} + \DeclareUnicodeCharacter{1E44}{\dotaccent{N}} + \DeclareUnicodeCharacter{1E45}{\dotaccent{n}} + \DeclareUnicodeCharacter{1E46}{\udotaccent{N}} + \DeclareUnicodeCharacter{1E47}{\udotaccent{n}} + \DeclareUnicodeCharacter{1E48}{\ubaraccent{N}} + \DeclareUnicodeCharacter{1E49}{\ubaraccent{n}} + + \DeclareUnicodeCharacter{1E54}{\'P} + \DeclareUnicodeCharacter{1E55}{\'p} + \DeclareUnicodeCharacter{1E56}{\dotaccent{P}} + \DeclareUnicodeCharacter{1E57}{\dotaccent{p}} + \DeclareUnicodeCharacter{1E58}{\dotaccent{R}} + \DeclareUnicodeCharacter{1E59}{\dotaccent{r}} + \DeclareUnicodeCharacter{1E5A}{\udotaccent{R}} + \DeclareUnicodeCharacter{1E5B}{\udotaccent{r}} + \DeclareUnicodeCharacter{1E5E}{\ubaraccent{R}} + \DeclareUnicodeCharacter{1E5F}{\ubaraccent{r}} + + \DeclareUnicodeCharacter{1E60}{\dotaccent{S}} + \DeclareUnicodeCharacter{1E61}{\dotaccent{s}} + \DeclareUnicodeCharacter{1E62}{\udotaccent{S}} + \DeclareUnicodeCharacter{1E63}{\udotaccent{s}} + \DeclareUnicodeCharacter{1E6A}{\dotaccent{T}} + \DeclareUnicodeCharacter{1E6B}{\dotaccent{t}} + \DeclareUnicodeCharacter{1E6C}{\udotaccent{T}} + \DeclareUnicodeCharacter{1E6D}{\udotaccent{t}} + \DeclareUnicodeCharacter{1E6E}{\ubaraccent{T}} + \DeclareUnicodeCharacter{1E6F}{\ubaraccent{t}} + + \DeclareUnicodeCharacter{1E7C}{\~V} + \DeclareUnicodeCharacter{1E7D}{\~v} + \DeclareUnicodeCharacter{1E7E}{\udotaccent{V}} + \DeclareUnicodeCharacter{1E7F}{\udotaccent{v}} + + \DeclareUnicodeCharacter{1E80}{\`W} + \DeclareUnicodeCharacter{1E81}{\`w} + \DeclareUnicodeCharacter{1E82}{\'W} + \DeclareUnicodeCharacter{1E83}{\'w} + \DeclareUnicodeCharacter{1E84}{\"W} + \DeclareUnicodeCharacter{1E85}{\"w} + \DeclareUnicodeCharacter{1E86}{\dotaccent{W}} + \DeclareUnicodeCharacter{1E87}{\dotaccent{w}} + \DeclareUnicodeCharacter{1E88}{\udotaccent{W}} + \DeclareUnicodeCharacter{1E89}{\udotaccent{w}} + \DeclareUnicodeCharacter{1E8A}{\dotaccent{X}} + \DeclareUnicodeCharacter{1E8B}{\dotaccent{x}} + \DeclareUnicodeCharacter{1E8C}{\"X} + \DeclareUnicodeCharacter{1E8D}{\"x} + \DeclareUnicodeCharacter{1E8E}{\dotaccent{Y}} + \DeclareUnicodeCharacter{1E8F}{\dotaccent{y}} + + \DeclareUnicodeCharacter{1E90}{\^Z} + \DeclareUnicodeCharacter{1E91}{\^z} + \DeclareUnicodeCharacter{1E92}{\udotaccent{Z}} + \DeclareUnicodeCharacter{1E93}{\udotaccent{z}} + \DeclareUnicodeCharacter{1E94}{\ubaraccent{Z}} + \DeclareUnicodeCharacter{1E95}{\ubaraccent{z}} + \DeclareUnicodeCharacter{1E96}{\ubaraccent{h}} + \DeclareUnicodeCharacter{1E97}{\"t} + \DeclareUnicodeCharacter{1E98}{\ringaccent{w}} + \DeclareUnicodeCharacter{1E99}{\ringaccent{y}} + + \DeclareUnicodeCharacter{1EA0}{\udotaccent{A}} + \DeclareUnicodeCharacter{1EA1}{\udotaccent{a}} + + \DeclareUnicodeCharacter{1EB8}{\udotaccent{E}} + \DeclareUnicodeCharacter{1EB9}{\udotaccent{e}} + \DeclareUnicodeCharacter{1EBC}{\~E} + \DeclareUnicodeCharacter{1EBD}{\~e} + + \DeclareUnicodeCharacter{1ECA}{\udotaccent{I}} + \DeclareUnicodeCharacter{1ECB}{\udotaccent{i}} + \DeclareUnicodeCharacter{1ECC}{\udotaccent{O}} + \DeclareUnicodeCharacter{1ECD}{\udotaccent{o}} + + \DeclareUnicodeCharacter{1EE4}{\udotaccent{U}} + \DeclareUnicodeCharacter{1EE5}{\udotaccent{u}} + + \DeclareUnicodeCharacter{1EF2}{\`Y} + \DeclareUnicodeCharacter{1EF3}{\`y} + \DeclareUnicodeCharacter{1EF4}{\udotaccent{Y}} + + \DeclareUnicodeCharacter{1EF8}{\~Y} + \DeclareUnicodeCharacter{1EF9}{\~y} + + \DeclareUnicodeCharacter{2013}{--} + \DeclareUnicodeCharacter{2014}{---} + \DeclareUnicodeCharacter{2018}{\quoteleft} + \DeclareUnicodeCharacter{2019}{\quoteright} + \DeclareUnicodeCharacter{201A}{\quotesinglbase} + \DeclareUnicodeCharacter{201C}{\quotedblleft} + \DeclareUnicodeCharacter{201D}{\quotedblright} + \DeclareUnicodeCharacter{201E}{\quotedblbase} + \DeclareUnicodeCharacter{2022}{\bullet} + \DeclareUnicodeCharacter{2026}{\dots} + \DeclareUnicodeCharacter{2039}{\guilsinglleft} + \DeclareUnicodeCharacter{203A}{\guilsinglright} + \DeclareUnicodeCharacter{20AC}{\euro} + + \DeclareUnicodeCharacter{2192}{\expansion} + \DeclareUnicodeCharacter{21D2}{\result} + + \DeclareUnicodeCharacter{2212}{\minus} + \DeclareUnicodeCharacter{2217}{\point} + \DeclareUnicodeCharacter{2261}{\equiv} +}% end of \utfeightchardefs + + +% US-ASCII character definitions. +\def\asciichardefs{% nothing need be done + \relax +} + +% Make non-ASCII characters printable again for compatibility with +% existing Texinfo documents that may use them, even without declaring a +% document encoding. +% +\setnonasciicharscatcode \other + + +\message{formatting,} + +\newdimen\defaultparindent \defaultparindent = 15pt + +\chapheadingskip = 15pt plus 4pt minus 2pt +\secheadingskip = 12pt plus 3pt minus 2pt +\subsecheadingskip = 9pt plus 2pt minus 2pt + +% Prevent underfull vbox error messages. +\vbadness = 10000 + +% Don't be so finicky about underfull hboxes, either. +\hbadness = 2000 + +% Following George Bush, get rid of widows and orphans. +\widowpenalty=10000 +\clubpenalty=10000 + +% Use TeX 3.0's \emergencystretch to help line breaking, but if we're +% using an old version of TeX, don't do anything. We want the amount of +% stretch added to depend on the line length, hence the dependence on +% \hsize. We call this whenever the paper size is set. +% +\def\setemergencystretch{% + \ifx\emergencystretch\thisisundefined + % Allow us to assign to \emergencystretch anyway. + \def\emergencystretch{\dimen0}% + \else + \emergencystretch = .15\hsize + \fi +} + +% Parameters in order: 1) textheight; 2) textwidth; +% 3) voffset; 4) hoffset; 5) binding offset; 6) topskip; +% 7) physical page height; 8) physical page width. +% +% We also call \setleading{\textleading}, so the caller should define +% \textleading. The caller should also set \parskip. +% +\def\internalpagesizes#1#2#3#4#5#6#7#8{% + \voffset = #3\relax + \topskip = #6\relax + \splittopskip = \topskip + % + \vsize = #1\relax + \advance\vsize by \topskip + \outervsize = \vsize + \advance\outervsize by 2\topandbottommargin + \pageheight = \vsize + % + \hsize = #2\relax + \outerhsize = \hsize + \advance\outerhsize by 0.5in + \pagewidth = \hsize + % + \normaloffset = #4\relax + \bindingoffset = #5\relax + % + \ifpdf + \pdfpageheight #7\relax + \pdfpagewidth #8\relax + % if we don't reset these, they will remain at "1 true in" of + % whatever layout pdftex was dumped with. + \pdfhorigin = 1 true in + \pdfvorigin = 1 true in + \fi + % + \setleading{\textleading} + % + \parindent = \defaultparindent + \setemergencystretch +} + +% @letterpaper (the default). +\def\letterpaper{{\globaldefs = 1 + \parskip = 3pt plus 2pt minus 1pt + \textleading = 13.2pt + % + % If page is nothing but text, make it come out even. + \internalpagesizes{607.2pt}{6in}% that's 46 lines + {\voffset}{.25in}% + {\bindingoffset}{36pt}% + {11in}{8.5in}% +}} + +% Use @smallbook to reset parameters for 7x9.25 trim size. +\def\smallbook{{\globaldefs = 1 + \parskip = 2pt plus 1pt + \textleading = 12pt + % + \internalpagesizes{7.5in}{5in}% + {-.2in}{0in}% + {\bindingoffset}{16pt}% + {9.25in}{7in}% + % + \lispnarrowing = 0.3in + \tolerance = 700 + \hfuzz = 1pt + \contentsrightmargin = 0pt + \defbodyindent = .5cm +}} + +% Use @smallerbook to reset parameters for 6x9 trim size. +% (Just testing, parameters still in flux.) +\def\smallerbook{{\globaldefs = 1 + \parskip = 1.5pt plus 1pt + \textleading = 12pt + % + \internalpagesizes{7.4in}{4.8in}% + {-.2in}{-.4in}% + {0pt}{14pt}% + {9in}{6in}% + % + \lispnarrowing = 0.25in + \tolerance = 700 + \hfuzz = 1pt + \contentsrightmargin = 0pt + \defbodyindent = .4cm +}} + +% Use @afourpaper to print on European A4 paper. +\def\afourpaper{{\globaldefs = 1 + \parskip = 3pt plus 2pt minus 1pt + \textleading = 13.2pt + % + % Double-side printing via postscript on Laserjet 4050 + % prints double-sided nicely when \bindingoffset=10mm and \hoffset=-6mm. + % To change the settings for a different printer or situation, adjust + % \normaloffset until the front-side and back-side texts align. Then + % do the same for \bindingoffset. You can set these for testing in + % your texinfo source file like this: + % @tex + % \global\normaloffset = -6mm + % \global\bindingoffset = 10mm + % @end tex + \internalpagesizes{673.2pt}{160mm}% that's 51 lines + {\voffset}{\hoffset}% + {\bindingoffset}{44pt}% + {297mm}{210mm}% + % + \tolerance = 700 + \hfuzz = 1pt + \contentsrightmargin = 0pt + \defbodyindent = 5mm +}} + +% Use @afivepaper to print on European A5 paper. +% From romildo@urano.iceb.ufop.br, 2 July 2000. +% He also recommends making @example and @lisp be small. +\def\afivepaper{{\globaldefs = 1 + \parskip = 2pt plus 1pt minus 0.1pt + \textleading = 12.5pt + % + \internalpagesizes{160mm}{120mm}% + {\voffset}{\hoffset}% + {\bindingoffset}{8pt}% + {210mm}{148mm}% + % + \lispnarrowing = 0.2in + \tolerance = 800 + \hfuzz = 1.2pt + \contentsrightmargin = 0pt + \defbodyindent = 2mm + \tableindent = 12mm +}} + +% A specific text layout, 24x15cm overall, intended for A4 paper. +\def\afourlatex{{\globaldefs = 1 + \afourpaper + \internalpagesizes{237mm}{150mm}% + {\voffset}{4.6mm}% + {\bindingoffset}{7mm}% + {297mm}{210mm}% + % + % Must explicitly reset to 0 because we call \afourpaper. + \globaldefs = 0 +}} + +% Use @afourwide to print on A4 paper in landscape format. +\def\afourwide{{\globaldefs = 1 + \afourpaper + \internalpagesizes{241mm}{165mm}% + {\voffset}{-2.95mm}% + {\bindingoffset}{7mm}% + {297mm}{210mm}% + \globaldefs = 0 +}} + +% @pagesizes TEXTHEIGHT[,TEXTWIDTH] +% Perhaps we should allow setting the margins, \topskip, \parskip, +% and/or leading, also. Or perhaps we should compute them somehow. +% +\parseargdef\pagesizes{\pagesizesyyy #1,,\finish} +\def\pagesizesyyy#1,#2,#3\finish{{% + \setbox0 = \hbox{\ignorespaces #2}\ifdim\wd0 > 0pt \hsize=#2\relax \fi + \globaldefs = 1 + % + \parskip = 3pt plus 2pt minus 1pt + \setleading{\textleading}% + % + \dimen0 = #1\relax + \advance\dimen0 by \voffset + % + \dimen2 = \hsize + \advance\dimen2 by \normaloffset + % + \internalpagesizes{#1}{\hsize}% + {\voffset}{\normaloffset}% + {\bindingoffset}{44pt}% + {\dimen0}{\dimen2}% +}} + +% Set default to letter. +% +\letterpaper + + +\message{and turning on texinfo input format.} + +% Define macros to output various characters with catcode for normal text. +\catcode`\"=\other +\catcode`\~=\other +\catcode`\^=\other +\catcode`\_=\other +\catcode`\|=\other +\catcode`\<=\other +\catcode`\>=\other +\catcode`\+=\other +\catcode`\$=\other +\def\normaldoublequote{"} +\def\normaltilde{~} +\def\normalcaret{^} +\def\normalunderscore{_} +\def\normalverticalbar{|} +\def\normalless{<} +\def\normalgreater{>} +\def\normalplus{+} +\def\normaldollar{$}%$ font-lock fix + +% This macro is used to make a character print one way in \tt +% (where it can probably be output as-is), and another way in other fonts, +% where something hairier probably needs to be done. +% +% #1 is what to print if we are indeed using \tt; #2 is what to print +% otherwise. Since all the Computer Modern typewriter fonts have zero +% interword stretch (and shrink), and it is reasonable to expect all +% typewriter fonts to have this, we can check that font parameter. +% +\def\ifusingtt#1#2{\ifdim \fontdimen3\font=0pt #1\else #2\fi} + +% Same as above, but check for italic font. Actually this also catches +% non-italic slanted fonts since it is impossible to distinguish them from +% italic fonts. But since this is only used by $ and it uses \sl anyway +% this is not a problem. +\def\ifusingit#1#2{\ifdim \fontdimen1\font>0pt #1\else #2\fi} + +% Turn off all special characters except @ +% (and those which the user can use as if they were ordinary). +% Most of these we simply print from the \tt font, but for some, we can +% use math or other variants that look better in normal text. + +\catcode`\"=\active +\def\activedoublequote{{\tt\char34}} +\let"=\activedoublequote +\catcode`\~=\active +\def~{{\tt\char126}} +\chardef\hat=`\^ +\catcode`\^=\active +\def^{{\tt \hat}} + +\catcode`\_=\active +\def_{\ifusingtt\normalunderscore\_} +\let\realunder=_ +% Subroutine for the previous macro. +\def\_{\leavevmode \kern.07em \vbox{\hrule width.3em height.1ex}\kern .07em } + +\catcode`\|=\active +\def|{{\tt\char124}} +\chardef \less=`\< +\catcode`\<=\active +\def<{{\tt \less}} +\chardef \gtr=`\> +\catcode`\>=\active +\def>{{\tt \gtr}} +\catcode`\+=\active +\def+{{\tt \char 43}} +\catcode`\$=\active +\def${\ifusingit{{\sl\$}}\normaldollar}%$ font-lock fix + +% If a .fmt file is being used, characters that might appear in a file +% name cannot be active until we have parsed the command line. +% So turn them off again, and have \everyjob (or @setfilename) turn them on. +% \otherifyactive is called near the end of this file. +\def\otherifyactive{\catcode`+=\other \catcode`\_=\other} + +% Used sometimes to turn off (effectively) the active characters even after +% parsing them. +\def\turnoffactive{% + \normalturnoffactive + \otherbackslash +} + +\catcode`\@=0 + +% \backslashcurfont outputs one backslash character in current font, +% as in \char`\\. +\global\chardef\backslashcurfont=`\\ +\global\let\rawbackslashxx=\backslashcurfont % let existing .??s files work + +% \realbackslash is an actual character `\' with catcode other, and +% \doublebackslash is two of them (for the pdf outlines). +{\catcode`\\=\other @gdef@realbackslash{\} @gdef@doublebackslash{\\}} + +% In texinfo, backslash is an active character; it prints the backslash +% in fixed width font. +\catcode`\\=\active +@def@normalbackslash{{@tt@backslashcurfont}} +% On startup, @fixbackslash assigns: +% @let \ = @normalbackslash + +% \rawbackslash defines an active \ to do \backslashcurfont. +% \otherbackslash defines an active \ to be a literal `\' character with +% catcode other. +@gdef@rawbackslash{@let\=@backslashcurfont} +@gdef@otherbackslash{@let\=@realbackslash} + +% Same as @turnoffactive except outputs \ as {\tt\char`\\} instead of +% the literal character `\'. +% +@def@normalturnoffactive{% + @let\=@normalbackslash + @let"=@normaldoublequote + @let~=@normaltilde + @let^=@normalcaret + @let_=@normalunderscore + @let|=@normalverticalbar + @let<=@normalless + @let>=@normalgreater + @let+=@normalplus + @let$=@normaldollar %$ font-lock fix + @unsepspaces +} + +% Make _ and + \other characters, temporarily. +% This is canceled by @fixbackslash. +@otherifyactive + +% If a .fmt file is being used, we don't want the `\input texinfo' to show up. +% That is what \eatinput is for; after that, the `\' should revert to printing +% a backslash. +% +@gdef@eatinput input texinfo{@fixbackslash} +@global@let\ = @eatinput + +% On the other hand, perhaps the file did not have a `\input texinfo'. Then +% the first `\' in the file would cause an error. This macro tries to fix +% that, assuming it is called before the first `\' could plausibly occur. +% Also turn back on active characters that might appear in the input +% file name, in case not using a pre-dumped format. +% +@gdef@fixbackslash{% + @ifx\@eatinput @let\ = @normalbackslash @fi + @catcode`+=@active + @catcode`@_=@active +} + +% Say @foo, not \foo, in error messages. +@escapechar = `@@ + +% These look ok in all fonts, so just make them not special. +@catcode`@& = @other +@catcode`@# = @other +@catcode`@% = @other + + +@c Local variables: +@c eval: (add-hook 'write-file-hooks 'time-stamp) +@c page-delimiter: "^\\\\message" +@c time-stamp-start: "def\\\\texinfoversion{" +@c time-stamp-format: "%:y-%02m-%02d.%02H" +@c time-stamp-end: "}" +@c End: + +@c vim:sw=2: + +@ignore + arch-tag: e1b36e32-c96e-4135-a41a-0b2efa2ea115 +@end ignore diff --git a/lang/python/examples/assuan.py b/lang/python/examples/assuan.py index dd42ad4..6784c9e 100644 --- a/lang/python/examples/assuan.py +++ b/lang/python/examples/assuan.py @@ -14,14 +14,14 @@ # # You should have received a copy of the GNU General Public License # along with this program; if not, see <http://www.gnu.org/licenses/>. - """Demonstrate the use of the Assuan protocol engine""" from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import gpg +del absolute_import, print_function, unicode_literals + with gpg.Context(protocol=gpg.constants.protocol.ASSUAN) as c: # Invoke the pinentry to get a confirmation. err = c.assuan_transact(['GET_CONFIRMATION', 'Hello there']) diff --git a/lang/python/examples/decryption-filter.py b/lang/python/examples/decryption-filter.py index 987dfd1..4d99330 100644 --- a/lang/python/examples/decryption-filter.py +++ b/lang/python/examples/decryption-filter.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright (C) 2016 g10 Code GmbH +# Copyright (C) 2016, 2018 g10 Code GmbH # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ # # You should have received a copy of the GNU General Public License # along with this program; if not, see <http://www.gnu.org/licenses/>. - """A decryption filter This demonstrates decryption using gpg3 in three lines of code. To @@ -25,8 +24,10 @@ be used like this: """ from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import sys import gpg + +del absolute_import, print_function, unicode_literals + gpg.Context().decrypt(sys.stdin, sink=sys.stdout) diff --git a/lang/python/examples/delkey.py b/lang/python/examples/delkey.py index 12510f3..30b3145 100755 --- a/lang/python/examples/delkey.py +++ b/lang/python/examples/delkey.py @@ -20,10 +20,11 @@ # It deletes keys for joe@example.org generated by genkey.py script from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import gpg +del absolute_import, print_function, unicode_literals + with gpg.Context() as c: # Note: We must not modify the key store during iteration, # therefore, we explicitly make a list. diff --git a/lang/python/examples/exportimport.py b/lang/python/examples/exportimport.py index d84a01c..36ced57 100755 --- a/lang/python/examples/exportimport.py +++ b/lang/python/examples/exportimport.py @@ -20,12 +20,13 @@ # It uses keys for joe+gpg@example.org generated by genkey.py script from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import sys import os import gpg +del absolute_import, print_function, unicode_literals + user = "joe+gpg@example.org" with gpg.Context(armor=True) as c, gpg.Data() as expkey: diff --git a/lang/python/examples/genkey.py b/lang/python/examples/genkey.py index a043500..710a530 100755 --- a/lang/python/examples/genkey.py +++ b/lang/python/examples/genkey.py @@ -18,10 +18,11 @@ # along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import gpg +del absolute_import, print_function, unicode_literals + # This is the example from the GPGME manual. parms = """<GnupgKeyParms format="internal"> diff --git a/lang/python/examples/howto/add-userid.py b/lang/python/examples/howto/add-userid.py index b868979..0c7bb89 100755 --- a/lang/python/examples/howto/add-userid.py +++ b/lang/python/examples/howto/add-userid.py @@ -18,7 +18,7 @@ from __future__ import absolute_import, division, unicode_literals # 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 General Public License and the GNU -# Lesser General Public Licensefor more details. +# Lesser General Public License for more details. # # You should have received a copy of the GNU General Public License and the GNU # Lesser General Public along with this program; if not, see diff --git a/lang/python/examples/howto/advanced/cython/keycount.pyx b/lang/python/examples/howto/advanced/cython/keycount.pyx new file mode 100755 index 0000000..2aa636d --- /dev/null +++ b/lang/python/examples/howto/advanced/cython/keycount.pyx @@ -0,0 +1,26 @@ +from __future__ import absolute_import + +import cython +import gpg + +c = gpg.Context() +seckeys = c.keylist(pattern=None, secret=True) +pubkeys = c.keylist(pattern=None, secret=False) + +seclist = list(seckeys) +secnum = len(seclist) + +publist = list(pubkeys) +pubnum = len(publist) + +if cython.compiled is True: + cc = "Powered by Cython compiled C code." +else: + cc = "Powered by Python." + +print(""" + Number of secret keys: {0} + Number of public keys: {1} + + {2} +""".format(secnum, pubnum, cc)) diff --git a/lang/python/examples/howto/advanced/cython/setup.py b/lang/python/examples/howto/advanced/cython/setup.py new file mode 100644 index 0000000..f8dce03 --- /dev/null +++ b/lang/python/examples/howto/advanced/cython/setup.py @@ -0,0 +1,6 @@ +from distutils.core import setup +from Cython.Build import cythonize + +setup( + ext_modules = cythonize("keycount.pyx", annotate=True) +) diff --git a/lang/python/examples/howto/clear-sign-file.py b/lang/python/examples/howto/clear-sign-file.py index 597bbc5..9d350e2 100755 --- a/lang/python/examples/howto/clear-sign-file.py +++ b/lang/python/examples/howto/clear-sign-file.py @@ -18,7 +18,7 @@ from __future__ import absolute_import, division, unicode_literals # 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 General Public License and the GNU -# Lesser General Public Licensefor more details. +# Lesser General Public License for more details. # # You should have received a copy of the GNU General Public License and the GNU # Lesser General Public along with this program; if not, see diff --git a/lang/python/examples/howto/create-key.py b/lang/python/examples/howto/create-key.py index 429ab1f..02d1cb3 100755 --- a/lang/python/examples/howto/create-key.py +++ b/lang/python/examples/howto/create-key.py @@ -18,7 +18,7 @@ from __future__ import absolute_import, division, unicode_literals # 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 General Public License and the GNU -# Lesser General Public Licensefor more details. +# Lesser General Public License for more details. # # You should have received a copy of the GNU General Public License and the GNU # Lesser General Public along with this program; if not, see diff --git a/lang/python/examples/howto/decrypt-file.py b/lang/python/examples/howto/decrypt-file.py index 60a050b..8eba1f2 100755 --- a/lang/python/examples/howto/decrypt-file.py +++ b/lang/python/examples/howto/decrypt-file.py @@ -18,7 +18,7 @@ from __future__ import absolute_import, division, unicode_literals # 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 General Public License and the GNU -# Lesser General Public Licensefor more details. +# Lesser General Public License for more details. # # You should have received a copy of the GNU General Public License and the GNU # Lesser General Public along with this program; if not, see @@ -32,13 +32,20 @@ if len(sys.argv) == 3: newfile = sys.argv[2] elif len(sys.argv) == 2: ciphertext = sys.argv[1] - newfile = input("Enter path and filename of file to save decrypted data to: ") + newfile = input("Enter path and filename to save decrypted data to: ") else: ciphertext = input("Enter path and filename of encrypted file: ") - newfile = input("Enter path and filename of file to save decrypted data to: ") + newfile = input("Enter path and filename to save decrypted data to: ") with open(ciphertext, "rb") as cfile: - plaintext, result, verify_result = gpg.Context().decrypt(cfile) + try: + plaintext, result, verify_result = gpg.Context().decrypt(cfile) + except gpg.errors.GPGMEError as e: + plaintext = None + print(e) -with open(newfile, "wb") as nfile: - nfile.write(plaintext) +if plaintext is not None: + with open(newfile, "wb") as nfile: + nfile.write(plaintext) +else: + pass diff --git a/lang/python/examples/howto/detach-sign-file.py b/lang/python/examples/howto/detach-sign-file.py index 99fbe65..a0c5a2a 100755 --- a/lang/python/examples/howto/detach-sign-file.py +++ b/lang/python/examples/howto/detach-sign-file.py @@ -18,7 +18,7 @@ from __future__ import absolute_import, division, unicode_literals # 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 General Public License and the GNU -# Lesser General Public Licensefor more details. +# Lesser General Public License for more details. # # You should have received a copy of the GNU General Public License and the GNU # Lesser General Public along with this program; if not, see diff --git a/lang/python/examples/howto/encrypt-file.py b/lang/python/examples/howto/encrypt-file.py index ad4e1ce..8951cb5 100755 --- a/lang/python/examples/howto/encrypt-file.py +++ b/lang/python/examples/howto/encrypt-file.py @@ -3,6 +3,9 @@ from __future__ import absolute_import, division, unicode_literals +import gpg +import sys + # Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> # # This program is free software; you can redistribute it and/or modify it under @@ -18,15 +21,12 @@ from __future__ import absolute_import, division, unicode_literals # 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 General Public License and the GNU -# Lesser General Public Licensefor more details. +# Lesser General Public License for more details. # # You should have received a copy of the GNU General Public License and the GNU # Lesser General Public along with this program; if not, see # <http://www.gnu.org/licenses/>. -import gpg -import sys - """ Encrypts a file to a specified key. If entering both the key and the filename on the command line, the key must be entered first. @@ -55,7 +55,7 @@ with open(filename, "rb") as f: with gpg.Context(armor=True) as ca: try: ciphertext, result, sign_result = ca.encrypt(text, recipients=rkey, - sign=False) + sign=False) with open("{0}.asc".format(filename), "wb") as fa: fa.write(ciphertext) except gpg.errors.InvalidRecipients as e: @@ -64,7 +64,7 @@ with gpg.Context(armor=True) as ca: with gpg.Context() as cg: try: ciphertext, result, sign_result = cg.encrypt(text, recipients=rkey, - sign=False) + sign=False) with open("{0}.gpg".format(filename), "wb") as fg: fg.write(ciphertext) except gpg.errors.InvalidRecipients as e: diff --git a/lang/python/examples/howto/encrypt-sign-file.py b/lang/python/examples/howto/encrypt-sign-file.py index 41aaac8..6610ee0 100755 --- a/lang/python/examples/howto/encrypt-sign-file.py +++ b/lang/python/examples/howto/encrypt-sign-file.py @@ -3,6 +3,9 @@ from __future__ import absolute_import, division, unicode_literals +import gpg +import sys + # Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> # # This program is free software; you can redistribute it and/or modify it under @@ -18,15 +21,12 @@ from __future__ import absolute_import, division, unicode_literals # 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 General Public License and the GNU -# Lesser General Public Licensefor more details. +# Lesser General Public License for more details. # # You should have received a copy of the GNU General Public License and the GNU # Lesser General Public along with this program; if not, see # <http://www.gnu.org/licenses/>. -import gpg -import sys - """ Signs and encrypts a file to a specified key. If entering both the key and the filename on the command line, the key must be entered first. @@ -58,13 +58,13 @@ with open(filename, "rb") as f: with gpg.Context(armor=True) as ca: ciphertext, result, sign_result = ca.encrypt(text, recipients=rkey, always_trust=True, - add_encrypt_to=True) + add_encrypt_to=True) with open("{0}.asc".format(filename), "wb") as fa: fa.write(ciphertext) with gpg.Context() as cg: ciphertext, result, sign_result = cg.encrypt(text, recipients=rkey, always_trust=True, - add_encrypt_to=True) + add_encrypt_to=True) with open("{0}.gpg".format(filename), "wb") as fg: fg.write(ciphertext) diff --git a/lang/python/examples/howto/encrypt-to-group-gullible.py b/lang/python/examples/howto/encrypt-to-group-gullible.py new file mode 100755 index 0000000..5ba82bc --- /dev/null +++ b/lang/python/examples/howto/encrypt-to-group-gullible.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +import gpg +import sys +from groups import group_lists + +# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 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 General Public License and the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# <http://www.gnu.org/licenses/>. + +""" +Uses the groups module to encrypt to multiple recipients. + +""" + +c = gpg.Context(armor=True) + +if len(sys.argv) > 3: + group_id = sys.argv[1] + filepath = sys.argv[2:] +elif len(sys.argv) == 3: + group_id = sys.argv[1] + filepath = sys.argv[2] +elif len(sys.argv) == 2: + group_id = sys.argv[1] + filepath = input("Enter the filename to encrypt: ") +else: + group_id = input("Enter the group name to encrypt to: ") + filepath = input("Enter the filename to encrypt: ") + +with open(filepath, "rb") as f: + text = f.read() + +for i in range(len(group_lists)): + if group_lists[i][0] == group_id: + klist = group_lists[i][1] + else: + klist = None + +logrus = [] + +if klist is not None: + for i in range(len(klist)): + apattern = list(c.keylist(pattern=klist[i], secret=False)) + if apattern[0].can_encrypt == 1: + logrus.append(apattern[0]) + else: + pass + try: + ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, + add_encrypt_to=True) + except: + ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, + add_encrypt_to=True, + always_trust=True) + with open("{0}.asc".format(filepath), "wb") as f: + f.write(ciphertext) +else: + pass + +# EOF diff --git a/lang/python/examples/howto/encrypt-to-group-trustno1.py b/lang/python/examples/howto/encrypt-to-group-trustno1.py new file mode 100755 index 0000000..680c9ea --- /dev/null +++ b/lang/python/examples/howto/encrypt-to-group-trustno1.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +import gpg +import sys +from groups import group_lists + +# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 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 General Public License and the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# <http://www.gnu.org/licenses/>. + +""" +Uses the groups module to encrypt to multiple recipients. + +""" + +c = gpg.Context(armor=True) + +if len(sys.argv) > 3: + group_id = sys.argv[1] + filepath = sys.argv[2:] +elif len(sys.argv) == 3: + group_id = sys.argv[1] + filepath = sys.argv[2] +elif len(sys.argv) == 2: + group_id = sys.argv[1] + filepath = input("Enter the filename to encrypt: ") +else: + group_id = input("Enter the group name to encrypt to: ") + filepath = input("Enter the filename to encrypt: ") + +with open(filepath, "rb") as f: + text = f.read() + +for i in range(len(group_lists)): + if group_lists[i][0] == group_id: + klist = group_lists[i][1] + else: + klist = None + +logrus = [] + +if klist is not None: + for i in range(len(klist)): + apattern = list(c.keylist(pattern=klist[i], secret=False)) + if apattern[0].can_encrypt == 1: + logrus.append(apattern[0]) + else: + pass + try: + ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, + add_encrypt_to=True) + except gpg.errors.InvalidRecipients as e: + for i in range(len(e.recipients)): + for n in range(len(logrus)): + if logrus[n].fpr == e.recipients[i].fpr: + logrus.remove(logrus[n]) + else: + pass + try: + ciphertext, result, sign_result = c.encrypt(text, + recipients=logrus, + add_encrypt_to=True) + except: + pass + with open("{0}.asc".format(filepath), "wb") as f: + f.write(ciphertext) +else: + pass + +# EOF diff --git a/lang/python/examples/howto/encrypt-to-group.py b/lang/python/examples/howto/encrypt-to-group.py new file mode 100755 index 0000000..e4ef1b6 --- /dev/null +++ b/lang/python/examples/howto/encrypt-to-group.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +import gpg +import sys +from groups import group_lists + +# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 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 General Public License and the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# <http://www.gnu.org/licenses/>. + +""" +Uses the groups module to encrypt to multiple recipients. + +""" + +c = gpg.Context(armor=True) + +if len(sys.argv) > 3: + group_id = sys.argv[1] + filepath = sys.argv[2:] +elif len(sys.argv) == 3: + group_id = sys.argv[1] + filepath = sys.argv[2] +elif len(sys.argv) == 2: + group_id = sys.argv[1] + filepath = input("Enter the filename to encrypt: ") +else: + group_id = input("Enter the group name to encrypt to: ") + filepath = input("Enter the filename to encrypt: ") + +with open(filepath, "rb") as f: + text = f.read() + +for i in range(len(group_lists)): + if group_lists[i][0] == group_id: + klist = group_lists[i][1] + else: + klist = None + +logrus = [] + +if klist is not None: + for i in range(len(klist)): + apattern = list(c.keylist(pattern=klist[i], secret=False)) + if apattern[0].can_encrypt == 1: + logrus.append(apattern[0]) + else: + pass + try: + ciphertext, result, sign_result = c.encrypt(text, recipients=logrus, + add_encrypt_to=True) + except gpg.errors.InvalidRecipients as e: + for i in range(len(e.recipients)): + for n in range(len(logrus)): + if logrus[n].fpr == e.recipients[i].fpr: + logrus.remove(logrus[n]) + else: + pass + try: + ciphertext, result, sign_result = c.encrypt(text, + recipients=logrus, + add_encrypt_to=True, + always_trust=True) + except: + pass + with open("{0}.asc".format(filepath), "wb") as f: + f.write(ciphertext) +else: + pass + +# EOF diff --git a/lang/python/examples/howto/export-key.py b/lang/python/examples/howto/export-key.py new file mode 100755 index 0000000..ff3345a --- /dev/null +++ b/lang/python/examples/howto/export-key.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +import gpg +import os.path +import sys + +# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 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 General Public License and the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# <http://www.gnu.org/licenses/>. + +print(""" +This script exports one or more public keys. +""") + +c = gpg.Context(armor=True) + +if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] +elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") +elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + keyfile = input("Enter the path and filename to save the key(s) to: ") + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if len(homedir) == 0: + homedir = None +elif homedir.startswith("~"): + userdir = os.path.expanduser(homedir) + if os.path.exists(userdir) is True: + homedir = os.path.realpath(userdir) + else: + homedir = None +else: + homedir = os.path.realpath(homedir) + +if homedir is not None and os.path.exists(homedir) is False: + homedir = None +elif homedir is not None and os.path.exists(homedir) is True: + if os.path.isdir(homedir) is False: + homedir = None + else: + pass + +if homedir is not None: + c.home_dir = homedir +else: + pass + +try: + result = c.key_export(pattern=logrus) +except: + result = c.key_export(pattern=None) + +if result is not None: + with open(keyfile, "wb") as f: + f.write(result) +else: + pass diff --git a/lang/python/examples/howto/export-minimised-key.py b/lang/python/examples/howto/export-minimised-key.py new file mode 100755 index 0000000..ffd7524 --- /dev/null +++ b/lang/python/examples/howto/export-minimised-key.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +import gpg +import os.path +import sys + +# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 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 General Public License and the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# <http://www.gnu.org/licenses/>. + +print(""" +This script exports one or more public keys in minimised form. +""") + +c = gpg.Context(armor=True) + +if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] +elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") +elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + keyfile = input("Enter the path and filename to save the key(s) to: ") + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if len(homedir) == 0: + homedir = None +elif homedir.startswith("~"): + userdir = os.path.expanduser(homedir) + if os.path.exists(userdir) is True: + homedir = os.path.realpath(userdir) + else: + homedir = None +else: + homedir = os.path.realpath(homedir) + +if homedir is not None and os.path.exists(homedir) is False: + homedir = None +elif homedir is not None and os.path.exists(homedir) is True: + if os.path.isdir(homedir) is False: + homedir = None + else: + pass + +if homedir is not None: + c.home_dir = homedir +else: + pass + +try: + result = c.key_export_minimal(pattern=logrus) +except: + result = c.key_export_minimal(pattern=None) + +if result is not None: + with open(keyfile, "wb") as f: + f.write(result) +else: + pass diff --git a/lang/python/examples/howto/export-secret-key.py b/lang/python/examples/howto/export-secret-key.py new file mode 100755 index 0000000..af1aa70 --- /dev/null +++ b/lang/python/examples/howto/export-secret-key.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +import gpg +import os +import os.path +import sys + +# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 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 General Public License and the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# <http://www.gnu.org/licenses/>. + +print(""" +This script exports one or more secret keys. + +The gpg-agent and pinentry are invoked to authorise the export. +""") + +c = gpg.Context(armor=True) + +if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] +elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") +elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + keyfile = input("Enter the path and filename to save the secret key to: ") + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if len(homedir) == 0: + homedir = None +elif homedir.startswith("~"): + userdir = os.path.expanduser(homedir) + if os.path.exists(userdir) is True: + homedir = os.path.realpath(userdir) + else: + homedir = None +else: + homedir = os.path.realpath(homedir) + +if homedir is not None and os.path.exists(homedir) is False: + homedir = None +elif homedir is not None and os.path.exists(homedir) is True: + if os.path.isdir(homedir) is False: + homedir = None + else: + pass + +if homedir is not None: + c.home_dir = homedir +else: + pass + +try: + result = c.key_export_secret(pattern=logrus) +except: + result = c.key_export_secret(pattern=None) + +if result is not None: + with open(keyfile, "wb") as f: + f.write(result) + os.chmod(keyfile, 0o600) +else: + pass diff --git a/lang/python/examples/howto/export-secret-keys.py b/lang/python/examples/howto/export-secret-keys.py new file mode 100755 index 0000000..f4e5008 --- /dev/null +++ b/lang/python/examples/howto/export-secret-keys.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +import gpg +import os +import os.path +import subprocess +import sys + +# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 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 General Public License and the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# <http://www.gnu.org/licenses/>. + +print(""" +This script exports one or more secret keys as both ASCII armored and binary +file formats, saved in files within the user's GPG home directory. + +The gpg-agent and pinentry are invoked to authorise the export. +""") + +if sys.platform == "win32": + gpgconfcmd = "gpgconf.exe --list-dirs homedir" +else: + gpgconfcmd = "gpgconf --list-dirs homedir" + +a = gpg.Context(armor=True) +b = gpg.Context() +c = gpg.Context() + +if len(sys.argv) >= 4: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = sys.argv[3] +elif len(sys.argv) == 3: + keyfile = sys.argv[1] + logrus = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") +elif len(sys.argv) == 2: + keyfile = sys.argv[1] + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + keyfile = input("Enter the filename to save the secret key to: ") + logrus = input("Enter the UID matching the secret key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if len(homedir) == 0: + homedir = None +elif homedir.startswith("~"): + userdir = os.path.expanduser(homedir) + if os.path.exists(userdir) is True: + homedir = os.path.realpath(userdir) + else: + homedir = None +else: + homedir = os.path.realpath(homedir) + +if homedir is not None and os.path.exists(homedir) is False: + homedir = None +elif homedir is not None and os.path.exists(homedir) is True: + if os.path.isdir(homedir) is False: + homedir = None + else: + pass + +if homedir is not None: + c.home_dir = homedir +else: + pass + +if c.home_dir is not None: + if c.home_dir.endswith("/"): + gpgfile = "{0}{1}.gpg".format(c.home_dir, keyfile) + ascfile = "{0}{1}.asc".format(c.home_dir, keyfile) + else: + gpgfile = "{0}/{1}.gpg".format(c.home_dir, keyfile) + ascfile = "{0}/{1}.asc".format(c.home_dir, keyfile) +else: + if os.path.exists(os.environ["GNUPGHOME"]) is True: + hd = os.environ["GNUPGHOME"] + else: + try: + hd = subprocess.getoutput(gpgconfcmd) + except: + process = subprocess.Popen(gpgconfcmd.split(), + stdout=subprocess.PIPE) + procom = process.communicate() + if sys.version_info[0] == 2: + hd = procom[0].strip() + else: + hd = procom[0].decode().strip() + gpgfile = "{0}/{1}.gpg".format(hd, keyfile) + ascfile = "{0}/{1}.asc".format(hd, keyfile) + +try: + a_result = a.key_export_secret(pattern=logrus) + b_result = b.key_export_secret(pattern=logrus) +except: + a_result = a.key_export_secret(pattern=None) + b_result = b.key_export_secret(pattern=None) + +if a_result is not None: + with open(ascfile, "wb") as f: + f.write(a_result) + os.chmod(ascfile, 0o600) +else: + pass + +if b_result is not None: + with open(gpgfile, "wb") as f: + f.write(b_result) + os.chmod(gpgfile, 0o600) +else: + pass diff --git a/lang/python/examples/howto/groups.py b/lang/python/examples/howto/groups.py index 5e7fdf6..3d51b25 100644 --- a/lang/python/examples/howto/groups.py +++ b/lang/python/examples/howto/groups.py @@ -17,13 +17,14 @@ from __future__ import absolute_import, division, unicode_literals # 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 General Public License and the GNU -# Lesser General Public Licensefor more details. +# Lesser General Public License for more details. # # You should have received a copy of the GNU General Public License and the GNU # Lesser General Public along with this program; if not, see # <http://www.gnu.org/licenses/>. import subprocess +import sys """ Intended for use with other scripts. @@ -31,7 +32,20 @@ Intended for use with other scripts. Usage: from groups import group_lists """ -lines = subprocess.getoutput("gpgconf --list-options gpg").splitlines() +if sys.platform == "win32": + gpgconfcmd = "gpgconf.exe --list-options gpg" +else: + gpgconfcmd = "gpgconf --list-options gpg" + +try: + lines = subprocess.getoutput(gpgconfcmd).splitlines() +except: + process = subprocess.Popen(gpgconfcmd.split(), stdout=subprocess.PIPE) + procom = process.communicate() + if sys.version_info[0] == 2: + lines = procom[0].splitlines() + else: + lines = procom[0].decode().splitlines() for i in range(len(lines)): if lines[i].startswith("group") is True: @@ -41,10 +55,12 @@ for i in range(len(lines)): groups = line.split(":")[-1].replace('"', '').split(',') -group_lines = groups -for i in range(len(group_lines)): - group_lines[i] = group_lines[i].split("=") +group_lines = [] +group_lists = [] + +for i in range(len(groups)): + group_lines.append(groups[i].split("=")) + group_lists.append(groups[i].split("=")) -group_lists = group_lines for i in range(len(group_lists)): group_lists[i][1] = group_lists[i][1].split() diff --git a/lang/python/examples/howto/import-key.py b/lang/python/examples/howto/import-key.py new file mode 100755 index 0000000..ab52ba2 --- /dev/null +++ b/lang/python/examples/howto/import-key.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +import gpg +import os.path +import sys + +del absolute_import, division, unicode_literals + +# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 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 General Public License and the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# <http://www.gnu.org/licenses/>. + +print(""" +This script imports one or more public keys from a single file. +""") + +c = gpg.Context(armor=True) + +if len(sys.argv) >= 3: + keyfile = sys.argv[1] + homedir = sys.argv[2] +elif len(sys.argv) == 2: + keyfile = sys.argv[1] + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + keyfile = input("Enter the path and filename to import the key(s) from: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if homedir.startswith("~"): + if os.path.exists(os.path.expanduser(homedir)) is True: + c.home_dir = os.path.expanduser(homedir) + else: + pass +elif os.path.exists(homedir) is True: + c.home_dir = homedir +else: + pass + +if os.path.isfile(keyfile) is True: + with open(keyfile, "rb") as f: + incoming = f.read() + result = c.key_import(incoming) +else: + result = None + +if result is not None and hasattr(result, "considered") is False: + print(result) +elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" +The total number of keys considered for import was: {0} + + Number of keys revoked: {1} + Number of new signatures: {2} + Number of new subkeys: {3} + Number of new user IDs: {4} +Number of new secret keys: {5} + Number of unchanged keys: {6} + +The key IDs for all considered keys were: +""".format(num_keys, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") +elif result is None: + print("You must specify a key file to import.") diff --git a/lang/python/examples/howto/import-keys-hkp.py b/lang/python/examples/howto/import-keys-hkp.py new file mode 100755 index 0000000..d50e465 --- /dev/null +++ b/lang/python/examples/howto/import-keys-hkp.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +import gpg +import hkp4py +import sys + +# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 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 General Public License and the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# <http://www.gnu.org/licenses/>. + +print(""" +This script imports one or more public keys from the SKS keyservers. +""") + +c = gpg.Context() +server = hkp4py.KeyServer("hkps://hkps.pool.sks-keyservers.net") +results = [] + +if len(sys.argv) > 2: + pattern = " ".join(sys.argv[1:]) +elif len(sys.argv) == 2: + pattern = sys.argv[1] +else: + pattern = input("Enter the pattern to search for keys or user IDs: ") + +try: + keys = server.search(pattern) + print("Found {0} key(s).".format(len(keys))) +except Exception as e: + keys = [] + for logrus in pattern.split(): + if logrus.startswith("0x") is True: + key = server.search(logrus) + else: + key = server.search("0x{0}".format(logrus)) + keys.append(key[0]) + print("Found {0} key(s).".format(len(keys))) + +for key in keys: + import_result = c.key_import(key.key_blob) + results.append(import_result) + +for result in results: + if result is not None and hasattr(result, "considered") is False: + print(result) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" +The total number of keys considered for import was: {0} + + Number of keys revoked: {1} + Number of new signatures: {2} + Number of new subkeys: {3} + Number of new user IDs: {4} +Number of new secret keys: {5} + Number of unchanged keys: {6} + +The key IDs for all considered keys were: +""".format(num_keys, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + else: + pass diff --git a/lang/python/examples/howto/import-keys.py b/lang/python/examples/howto/import-keys.py new file mode 100755 index 0000000..d1a268e --- /dev/null +++ b/lang/python/examples/howto/import-keys.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +import gpg +import os.path +import requests + +# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 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 General Public License and the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# <http://www.gnu.org/licenses/>. + +print(""" +This script imports one or more public keys from the SKS keyservers. +""") + +c = gpg.Context() +url = "https://sks-keyservers.net/pks/lookup" +pattern = input("Enter the pattern to search for key or user IDs: ") +payload = {"op": "get", "search": pattern} + +r = requests.get(url, verify=True, params=payload) +result = c.key_import(r.content) + +if result is not None and hasattr(result, "considered") is False: + print(result) +elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" +The total number of keys considered for import was: {0} + + Number of keys revoked: {1} + Number of new signatures: {2} + Number of new subkeys: {3} + Number of new user IDs: {4} +Number of new secret keys: {5} + Number of unchanged keys: {6} + +The key IDs for all considered keys were: +""".format(num_keys, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") +else: + pass diff --git a/lang/python/examples/howto/keycount.py b/lang/python/examples/howto/keycount.py index 8e25454..af47272 100755 --- a/lang/python/examples/howto/keycount.py +++ b/lang/python/examples/howto/keycount.py @@ -18,7 +18,7 @@ from __future__ import absolute_import, division, unicode_literals # 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 General Public License and the GNU -# Lesser General Public Licensefor more details. +# Lesser General Public License for more details. # # You should have received a copy of the GNU General Public License and the GNU # Lesser General Public along with this program; if not, see diff --git a/lang/python/examples/howto/local-sign-group.py b/lang/python/examples/howto/local-sign-group.py new file mode 100755 index 0000000..0353646 --- /dev/null +++ b/lang/python/examples/howto/local-sign-group.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +import gpg +import os.path +import subprocess +import sys + +from groups import group_lists + +# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 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 General Public License and the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# <http://www.gnu.org/licenses/>. + +print(""" +This script applies a local signature or certification to every key in a group. + +Usage: local-sign-group.py <group name> [signing keyid] [gnupg homedir] +""") + +c = gpg.Context(armor=True) +mkfpr = None +defkey_fpr = None +enckey_fpr = None +to_certify = [] + +if len(sys.argv) >= 4: + clique = sys.argv[1] + sigkey = sys.argv[2] + homedir = sys.argv[3] +elif len(sys.argv) == 3: + clique = sys.argv[1] + sigkey = sys.argv[2] + homedir = input("Enter the GPG configuration directory path (optional): ") +elif len(sys.argv) == 2: + clique = sys.argv[1] + sigkey = input("Enter the key ID to sign with (conditionally optional): ") + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + clique = input("Enter the group matching the key(s) to locally sign: ") + sigkey = input("Enter the key ID to sign with (conditionally optional): ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if len(homedir) == 0: + homedir = None +elif homedir.startswith("~"): + userdir = os.path.expanduser(homedir) + if os.path.exists(userdir) is True: + homedir = os.path.realpath(userdir) + else: + homedir = None +else: + homedir = os.path.realpath(homedir) + +if homedir is not None and os.path.exists(homedir) is False: + homedir = None +elif homedir is not None and os.path.exists(homedir) is True: + if os.path.isdir(homedir) is False: + homedir = None + else: + pass + +if homedir is not None: + c.home_dir = homedir +else: + pass + +if len(sigkey) == 0: + sigkey = None +else: + pass + +if sys.platform == "win32": + gpgconfcmd = "gpgconf.exe --list-options gpg" +else: + gpgconfcmd = "gpgconf --list-options gpg" + +try: + lines = subprocess.getoutput(gpgconfcmd).splitlines() +except: + process = subprocess.Popen(gpgconfcmd.split(), stdout=subprocess.PIPE) + procom = process.communicate() + if sys.version_info[0] == 2: + lines = procom[0].splitlines() + else: + lines = procom[0].decode().splitlines() + +for i in range(len(lines)): + if lines[i].startswith("default-key") is True: + dline = lines[i] + elif lines[i].startswith("encrypt-to") is True: + eline = lines[i] + else: + pass + +defkey_fpr = dline.split(":")[-1].replace('"', '').split(',')[0].upper() +enckey_fpr = eline.split(":")[-1].replace('"', '').split(',')[0].upper() + +try: + dkey = c.keylist(pattern=defkey_fpr, secret=True) + dk = list(dkey) +except Exception as de: + print(de) + dk = None + print("No valid default key.") + +try: + ekey = c.keylist(pattern=defkey_fpr, secret=True) + ek = list(ekey) +except Exception as ee: + print(ee) + ek = None + print("No valid always encrypt to key.") + +if sigkey is not None: + mykey = c.keylist(pattern=sigkey, secret=True) + mk = list(mykey) + mkfpr = mk[0].fpr.upper() + c.signers = mk +else: + if dk is None and ek is not None: + c.signers = ek + else: + pass + +for group in group_lists: + if group[0] == clique: + for logrus in group[1]: + khole = c.keylist(pattern=logrus) + k = list(khole) + to_certify.append(k[0].fpr.upper()) + else: + pass + +if mkfpr is not None: + if to_certify.count(mkfpr) > 0: + for n in range(to_certify.count(mkfpr)): + to_certify.remove(mkfpr) + else: + pass +else: + pass + +if defkey_fpr is not None: + if to_certify.count(defkey_fpr) > 0: + for n in range(to_certify.count(defkey_fpr)): + to_certify.remove(defkey_fpr) + else: + pass +else: + pass + +if enckey_fpr is not None: + if to_certify.count(enckey_fpr) > 0: + for n in range(to_certify.count(enckey_fpr)): + to_certify.remove(enckey_fpr) + else: + pass +else: + pass + +for fpr in to_certify: + key = c.get_key(fpr) + c.key_sign(key, uids=None, expires_in=False, local=True) diff --git a/lang/python/examples/howto/mutt-groups.py b/lang/python/examples/howto/mutt-groups.py new file mode 100755 index 0000000..6572a21 --- /dev/null +++ b/lang/python/examples/howto/mutt-groups.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 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 General Public License and the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# <http://www.gnu.org/licenses/>. + +import sys +from groups import group_lists + +""" +Uses the groups module to generate Mutt crypt-hooks from gpg.conf. + +""" + +if len(sys.argv) >= 2: + hook_file = sys.argv[1] +else: + hook_file = input("Enter the filename to save the crypt-hooks in: ") + +with open(hook_file, "w") as f: + f.write("""# Change settings based upon message recipient +# +# send-hook [!]<pattern> <command> +# +# <command> is executed when sending mail to an address matching <pattern> +# +# crypt-hook regexp key-id +# The crypt-hook command provides a method by which you can +# specify the ID of the public key to be used when encrypting +# messages to a certain recipient. The meaning of "key ID" is to +# be taken broadly: This can be a different e-mail address, a +# numerical key ID, or even just an arbitrary search string. You +# may use multiple crypt-hooks with the same regexp; multiple +# matching crypt-hooks result in the use of multiple key-ids for a +# recipient. +""") + +for n in range(len(group_lists)): + rule = group_lists[n][0].replace(".", "\\\\.") + with open(hook_file, "a") as f: + f.write("\n") + f.write("# {0}\n".format(group_lists[n][0])) + for i in range(len(group_lists[n][1])): + f.write("crypt-hook {0} {1}\n".format(rule, group_lists[n][1][i])) diff --git a/lang/python/examples/howto/pmkey-import-alt.py b/lang/python/examples/howto/pmkey-import-alt.py new file mode 100755 index 0000000..c7822f3 --- /dev/null +++ b/lang/python/examples/howto/pmkey-import-alt.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +import gpg +import os.path +import requests +import sys + +del absolute_import, division, unicode_literals + +# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 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 General Public License and the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# <http://www.gnu.org/licenses/>. + +print(""" +This script searches the ProtonMail key server for the specified key and +imports it. Optionally enables specifying a different GnuPG home directory. + +Usage: pmkey-import-alt.py [search string] [homedir] +""") + +c = gpg.Context(armor=True) +url = "https://api.protonmail.ch/pks/lookup" +ksearch = [] + +if len(sys.argv) >= 3: + keyterm = sys.argv[1] + homedir = sys.argv[2] +elif len(sys.argv) == 2: + keyterm = sys.argv[1] + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + keyterm = input("Enter the key ID, UID or search string: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if homedir.startswith("~"): + if os.path.exists(os.path.expanduser(homedir)) is True: + c.home_dir = os.path.expanduser(homedir) + else: + pass +elif os.path.exists(homedir) is True: + c.home_dir = homedir +else: + pass + +if keyterm.count("@") == 2 and keyterm.startswith("@") is True: + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) +elif keyterm.count("@") == 1 and keyterm.startswith("@") is True: + ksearch.append("{0}@protonmail.com".format(keyterm[1:])) + ksearch.append("{0}@protonmail.ch".format(keyterm[1:])) + ksearch.append("{0}@pm.me".format(keyterm[1:])) +elif keyterm.count("@") == 0: + ksearch.append("{0}@protonmail.com".format(keyterm)) + ksearch.append("{0}@protonmail.ch".format(keyterm)) + ksearch.append("{0}@pm.me".format(keyterm)) +elif keyterm.count("@") == 2 and keyterm.startswith("@") is False: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) +elif keyterm.count("@") > 2: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) +else: + ksearch.append(keyterm) + +for k in ksearch: + payload = {"op": "get", "search": k} + try: + r = requests.get(url, verify=True, params=payload) + if r.ok is True: + result = c.key_import(r.content) + elif r.ok is False: + result = r.content + except Exception as e: + result = None + + if result is not None and hasattr(result, "considered") is False: + print("{0} for {1}".format(result.decode(), k)) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" +The total number of keys considered for import was: {0} + +With UIDs wholely or partially matching the following string: + + {1} + + Number of keys revoked: {2} + Number of new signatures: {3} + Number of new subkeys: {4} + Number of new user IDs: {5} +Number of new secret keys: {6} + Number of unchanged keys: {7} + +The key IDs for all considered keys were: +""".format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + elif result is None: + print(e) diff --git a/lang/python/examples/howto/pmkey-import-hkp-alt.py b/lang/python/examples/howto/pmkey-import-hkp-alt.py new file mode 100755 index 0000000..61fcd8d --- /dev/null +++ b/lang/python/examples/howto/pmkey-import-hkp-alt.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +import gpg +import hkp4py +import os.path +import sys + +del absolute_import, division, unicode_literals + +# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 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 General Public License and the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# <http://www.gnu.org/licenses/>. + +print(""" +This script searches the ProtonMail key server for the specified key and +imports it. Optionally enables specifying a different GnuPG home directory. + +Usage: pmkey-import-hkp.py [homedir] [search string] + or: pmkey-import-hkp.py [search string] +""") + +c = gpg.Context(armor=True) +server = hkp4py.KeyServer("hkps://api.protonmail.ch") +keyterms = [] +ksearch = [] +allkeys = [] +results = [] +paradox = [] +homeless = None + +if len(sys.argv) > 3: + homedir = sys.argv[1] + keyterms = sys.argv[2:] +elif len(sys.argv) == 3: + homedir = sys.argv[1] + keyterm = sys.argv[2] + keyterms.append(keyterm) +elif len(sys.argv) == 2: + homedir = "" + keyterm = sys.argv[1] + keyterms.append(keyterm) +else: + keyterm = input("Enter the key ID, UID or search string: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + keyterms.append(keyterm) + +if len(homedir) == 0: + homedir = None + homeless = False + +if homedir is not None: + if homedir.startswith("~"): + if os.path.exists(os.path.expanduser(homedir)) is True: + if os.path.isdir(os.path.expanduser(homedir)) is True: + c.home_dir = os.path.realpath(os.path.expanduser(homedir)) + else: + homeless = True + else: + homeless = True + elif os.path.exists(os.path.realpath(homedir)) is True: + if os.path.isdir(os.path.realpath(homedir)) is True: + c.home_dir = os.path.realpath(homedir) + else: + homeless = True + else: + homeless = True + +# First check to see if the homedir really is a homedir and if not, treat it as +# a search string. +if homeless is True: + keyterms.append(homedir) + c.home_dir = None +else: + pass + +for keyterm in keyterms: + if keyterm.count("@") == 2 and keyterm.startswith("@") is True: + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + elif keyterm.count("@") == 1 and keyterm.startswith("@") is True: + ksearch.append("{0}@protonmail.com".format(keyterm[1:])) + ksearch.append("{0}@protonmail.ch".format(keyterm[1:])) + ksearch.append("{0}@pm.me".format(keyterm[1:])) + elif keyterm.count("@") == 0: + ksearch.append("{0}@protonmail.com".format(keyterm)) + ksearch.append("{0}@protonmail.ch".format(keyterm)) + ksearch.append("{0}@pm.me".format(keyterm)) + elif keyterm.count("@") == 2 and keyterm.startswith("@") is False: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) + elif keyterm.count("@") > 2: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) + else: + ksearch.append(keyterm) + +for k in ksearch: + print("Checking for key for: {0}".format(k)) + try: + keys = server.search(k) + if isinstance(keys, list) is True: + for key in keys: + allkeys.append(key) + try: + import_result = c.key_import(key.key_blob) + except Exception as e: + import_result = c.key_import(key.key) + else: + paradox.append(keys) + import_result = None + except Exception as e: + import_result = None + results.append(import_result) + +for result in results: + if result is not None and hasattr(result, "considered") is False: + print("{0} for {1}".format(result.decode(), k)) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" +The total number of keys considered for import was: {0} + +With UIDs wholely or partially matching the following string: + + {1} + + Number of keys revoked: {2} + Number of new signatures: {3} + Number of new subkeys: {4} + Number of new user IDs: {5} +Number of new secret keys: {6} + Number of unchanged keys: {7} + +The key IDs for all considered keys were: +""".format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + elif result is None: + pass diff --git a/lang/python/examples/howto/pmkey-import-hkp.py b/lang/python/examples/howto/pmkey-import-hkp.py new file mode 100755 index 0000000..66223a9 --- /dev/null +++ b/lang/python/examples/howto/pmkey-import-hkp.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +import gpg +import hkp4py +import os.path +import sys + +del absolute_import, division, unicode_literals + +# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 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 General Public License and the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# <http://www.gnu.org/licenses/>. + +print(""" +This script searches the ProtonMail key server for the specified key and +imports it. + +Usage: pmkey-import-hkp.py [search strings] +""") + +c = gpg.Context(armor=True) +server = hkp4py.KeyServer("hkps://api.protonmail.ch") +keyterms = [] +ksearch = [] +allkeys = [] +results = [] +paradox = [] +homeless = None + +if len(sys.argv) > 2: + keyterms = sys.argv[1:] +elif len(sys.argv) == 2: + keyterm = sys.argv[1] + keyterms.append(keyterm) +else: + key_term = input("Enter the key ID, UID or search string: ") + keyterms = key_term.split() + +for keyterm in keyterms: + if keyterm.count("@") == 2 and keyterm.startswith("@") is True: + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + elif keyterm.count("@") == 1 and keyterm.startswith("@") is True: + ksearch.append("{0}@protonmail.com".format(keyterm[1:])) + ksearch.append("{0}@protonmail.ch".format(keyterm[1:])) + ksearch.append("{0}@pm.me".format(keyterm[1:])) + elif keyterm.count("@") == 0: + ksearch.append("{0}@protonmail.com".format(keyterm)) + ksearch.append("{0}@protonmail.ch".format(keyterm)) + ksearch.append("{0}@pm.me".format(keyterm)) + elif keyterm.count("@") == 2 and keyterm.startswith("@") is False: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) + elif keyterm.count("@") > 2: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) + else: + ksearch.append(keyterm) + +for k in ksearch: + print("Checking for key for: {0}".format(k)) + try: + keys = server.search(k) + if isinstance(keys, list) is True: + for key in keys: + allkeys.append(key) + try: + import_result = c.key_import(key.key_blob) + except Exception as e: + import_result = c.key_import(key.key) + else: + paradox.append(keys) + import_result = None + except Exception as e: + import_result = None + results.append(import_result) + +for result in results: + if result is not None and hasattr(result, "considered") is False: + print("{0} for {1}".format(result.decode(), k)) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" +The total number of keys considered for import was: {0} + +With UIDs wholely or partially matching the following string: + + {1} + + Number of keys revoked: {2} + Number of new signatures: {3} + Number of new subkeys: {4} + Number of new user IDs: {5} +Number of new secret keys: {6} + Number of unchanged keys: {7} + +The key IDs for all considered keys were: +""".format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + elif result is None: + pass diff --git a/lang/python/examples/howto/pmkey-import.py b/lang/python/examples/howto/pmkey-import.py new file mode 100755 index 0000000..58a01c8 --- /dev/null +++ b/lang/python/examples/howto/pmkey-import.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +import gpg +import requests +import sys + +del absolute_import, division, unicode_literals + +# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 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 General Public License and the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# <http://www.gnu.org/licenses/>. + +print(""" +This script searches the ProtonMail key server for the specified key and +imports it. + +Usage: pmkey-import.py [search string] +""") + +c = gpg.Context(armor=True) +url = "https://api.protonmail.ch/pks/lookup" +ksearch = [] + +if len(sys.argv) >= 2: + keyterm = sys.argv[1] +else: + keyterm = input("Enter the key ID, UID or search string: ") + +if keyterm.count("@") == 2 and keyterm.startswith("@") is True: + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) + ksearch.append(keyterm[1:]) +elif keyterm.count("@") == 1 and keyterm.startswith("@") is True: + ksearch.append("{0}@protonmail.com".format(keyterm[1:])) + ksearch.append("{0}@protonmail.ch".format(keyterm[1:])) + ksearch.append("{0}@pm.me".format(keyterm[1:])) +elif keyterm.count("@") == 0: + ksearch.append("{0}@protonmail.com".format(keyterm)) + ksearch.append("{0}@protonmail.ch".format(keyterm)) + ksearch.append("{0}@pm.me".format(keyterm)) +elif keyterm.count("@") == 2 and keyterm.startswith("@") is False: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) +elif keyterm.count("@") > 2: + uidlist = keyterm.split("@") + for uid in uidlist: + ksearch.append("{0}@protonmail.com".format(uid)) + ksearch.append("{0}@protonmail.ch".format(uid)) + ksearch.append("{0}@pm.me".format(uid)) +else: + ksearch.append(keyterm) + +for k in ksearch: + payload = {"op": "get", "search": k} + try: + r = requests.get(url, verify=True, params=payload) + if r.ok is True: + result = c.key_import(r.content) + elif r.ok is False: + result = r.content + except Exception as e: + result = None + + if result is not None and hasattr(result, "considered") is False: + print("{0} for {1}".format(result.decode(), k)) + elif result is not None and hasattr(result, "considered") is True: + num_keys = len(result.imports) + new_revs = result.new_revocations + new_sigs = result.new_signatures + new_subs = result.new_sub_keys + new_uids = result.new_user_ids + new_scrt = result.secret_imported + nochange = result.unchanged + print(""" +The total number of keys considered for import was: {0} + +With UIDs wholely or partially matching the following string: + + {1} + + Number of keys revoked: {2} + Number of new signatures: {3} + Number of new subkeys: {4} + Number of new user IDs: {5} +Number of new secret keys: {6} + Number of unchanged keys: {7} + +The key IDs for all considered keys were: +""".format(num_keys, k, new_revs, new_sigs, new_subs, new_uids, new_scrt, + nochange)) + for i in range(num_keys): + print(result.imports[i].fpr) + print("") + elif result is None: + print(e) diff --git a/lang/python/examples/howto/revoke-userid.py b/lang/python/examples/howto/revoke-userid.py index 7a3d190..be34748 100755 --- a/lang/python/examples/howto/revoke-userid.py +++ b/lang/python/examples/howto/revoke-userid.py @@ -18,7 +18,7 @@ from __future__ import absolute_import, division, unicode_literals # 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 General Public License and the GNU -# Lesser General Public Licensefor more details. +# Lesser General Public License for more details. # # You should have received a copy of the GNU General Public License and the GNU # Lesser General Public along with this program; if not, see diff --git a/lang/python/examples/howto/send-key-to-keyserver.py b/lang/python/examples/howto/send-key-to-keyserver.py new file mode 100755 index 0000000..261c8b6 --- /dev/null +++ b/lang/python/examples/howto/send-key-to-keyserver.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +import gpg +import hkp4py +import os.path +import sys + +# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 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 General Public License and the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# <http://www.gnu.org/licenses/>. + +print(""" +This script sends one or more public keys to the SKS keyservers and is +essentially a slight variation on the export-key.py script. + +The default is to send all keys if there is no pattern or search term. +""") + +c = gpg.Context(armor=True) +server = hkp4py.KeyServer("hkps://hkps.pool.sks-keyservers.net") + +if len(sys.argv) >= 3: + logrus = sys.argv[1] + homedir = sys.argv[2] +elif len(sys.argv) == 2: + logrus = sys.argv[1] + homedir = input("Enter the GPG configuration directory path (optional): ") +else: + logrus = input("Enter the UID matching the key(s) to export: ") + homedir = input("Enter the GPG configuration directory path (optional): ") + +if len(homedir) == 0: + homedir = None +elif homedir.startswith("~"): + userdir = os.path.expanduser(homedir) + if os.path.exists(userdir) is True: + homedir = os.path.realpath(userdir) + else: + homedir = None +else: + homedir = os.path.realpath(homedir) + +if homedir is not None and os.path.exists(homedir) is False: + homedir = None +elif homedir is not None and os.path.exists(homedir) is True: + if os.path.isdir(homedir) is False: + homedir = None + else: + pass + +if homedir is not None: + c.home_dir = homedir +else: + pass + +if len(logrus) > 0: + try: + export_result = c.key_export(pattern=logrus) + except Exception as e: + print(e) + export_result = None +else: + export_result = c.key_export(pattern=None) + +if export_result is not None: + try: + try: + send_result = server.add(export_result) + except: + send_result = server.add(export_result.decode()) + if send_result is not None: + print(send_result) + else: + pass + except Exception as e: + print(e) +else: + pass diff --git a/lang/python/examples/howto/sign-file.py b/lang/python/examples/howto/sign-file.py index 01006df..f680965 100755 --- a/lang/python/examples/howto/sign-file.py +++ b/lang/python/examples/howto/sign-file.py @@ -18,7 +18,7 @@ from __future__ import absolute_import, division, unicode_literals # 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 General Public License and the GNU -# Lesser General Public Licensefor more details. +# Lesser General Public License for more details. # # You should have received a copy of the GNU General Public License and the GNU # Lesser General Public along with this program; if not, see diff --git a/lang/python/examples/howto/sign-key.py b/lang/python/examples/howto/sign-key.py index b1afe13..6d32c57 100755 --- a/lang/python/examples/howto/sign-key.py +++ b/lang/python/examples/howto/sign-key.py @@ -18,7 +18,7 @@ from __future__ import absolute_import, division, unicode_literals # 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 General Public License and the GNU -# Lesser General Public Licensefor more details. +# Lesser General Public License for more details. # # You should have received a copy of the GNU General Public License and the GNU # Lesser General Public along with this program; if not, see diff --git a/lang/python/examples/howto/symcrypt-file.py b/lang/python/examples/howto/symcrypt-file.py new file mode 100755 index 0000000..bce9286 --- /dev/null +++ b/lang/python/examples/howto/symcrypt-file.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, division, unicode_literals + +import gpg +import sys + +# Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 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 General Public License and the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU General Public License and the GNU +# Lesser General Public along with this program; if not, see +# <http://www.gnu.org/licenses/>. + +""" +Symmetrically encrypts a file. Passphrase will be prompted for via Pinentry. + +Will produce both an ASCII armoured and GPG binary format copy of the encrypted +file. +""" + +if len(sys.argv) > 2: + filename = " ".join(sys.argv[1:]) +elif len(sys.argv) == 2: + filename = sys.argv[1] +else: + filename = input("Enter the path and filename to encrypt: ") + +with open(filename, "rb") as f: + text = f.read() + +with gpg.Context(armor=True) as ca: + try: + ciphertext, result, sign_result = ca.encrypt(text, passphrase=None, + sign=False) + with open("{0}.asc".format(filename), "wb") as fa: + fa.write(ciphertext) + except gpg.errors.GPGMEError as e: + print(e) + +with gpg.Context() as cg: + try: + ciphertext, result, sign_result = cg.encrypt(text, passphrase=None, + sign=False) + with open("{0}.gpg".format(filename), "wb") as fg: + fg.write(ciphertext) + except gpg.errors.GPGMEError as e: + print(e) diff --git a/lang/python/examples/howto/temp-homedir-config.py b/lang/python/examples/howto/temp-homedir-config.py index ddd7932..98bde36 100755 --- a/lang/python/examples/howto/temp-homedir-config.py +++ b/lang/python/examples/howto/temp-homedir-config.py @@ -3,6 +3,10 @@ from __future__ import absolute_import, division, unicode_literals +import os +import os.path +import sys + # Copyright (C) 2018 Ben McGinnes <ben@gnupg.org> # # This program is free software; you can redistribute it and/or modify it under @@ -18,16 +22,12 @@ from __future__ import absolute_import, division, unicode_literals # 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 General Public License and the GNU -# Lesser General Public Licensefor more details. +# Lesser General Public License for more details. # # You should have received a copy of the GNU General Public License and the GNU # Lesser General Public along with this program; if not, see # <http://www.gnu.org/licenses/>. -import os -import os.path -import sys - intro = """ This script creates a temporary directory to use as a homedir for testing key generation tasks with the correct permissions, along @@ -54,6 +54,13 @@ message telling you to specify a new directory name. There is no default directory name. """ +ciphers256 = "TWOFISH CAMELLIA256 AES256" +ciphers192 = "CAMELLIA192 AES192" +ciphers128 = "CAMELLIA128 AES" +ciphersBad = "BLOWFISH IDEA CAST5 3DES" +digests = "SHA512 SHA384 SHA256 SHA224 RIPEMD160 SHA1" +compress = "ZLIB BZIP2 ZIP Uncompressed" + gpgconf = """# gpg.conf settings for key generation: expert allow-freeform-uid @@ -63,11 +70,11 @@ tofu-default-policy unknown enable-large-rsa enable-dsa2 cert-digest-algo SHA512 -default-preference-list TWOFISH CAMELLIA256 AES256 CAMELLIA192 AES192 CAMELLIA128 AES BLOWFISH IDEA CAST5 3DES SHA512 SHA384 SHA256 SHA224 RIPEMD160 SHA1 ZLIB BZIP2 ZIP Uncompressed -personal-cipher-preferences TWOFISH CAMELLIA256 AES256 CAMELLIA192 AES192 CAMELLIA128 AES BLOWFISH IDEA CAST5 3DES -personal-digest-preferences SHA512 SHA384 SHA256 SHA224 RIPEMD160 SHA1 -personal-compress-preferences ZLIB BZIP2 ZIP Uncompressed -""" +default-preference-list {0} {1} {2} {3} {4} {5} +personal-cipher-preferences {0} {1} {2} {3} +personal-digest-preferences {4} +personal-compress-preferences {5} +""".format(ciphers256, ciphers192, ciphers128, ciphersBad, digests, compress) agentconf = """# gpg-agent.conf settings for key generation: default-cache-ttl 300 @@ -84,17 +91,17 @@ else: userdir = os.path.expanduser("~") if new_homedir.startswith("~"): - new_homdir.replace("~", "") + new_homedir.replace("~", "") else: pass if new_homedir.startswith("/"): - new_homdir.replace("/", "") + new_homedir.replace("/", "") else: pass if new_homedir.startswith("."): - new_homdir.replace(".", "_") + new_homedir.replace(".", "_") else: pass diff --git a/lang/python/examples/howto/verify-signatures.py b/lang/python/examples/howto/verify-signatures.py index 8aafc3b..aa4331c 100755 --- a/lang/python/examples/howto/verify-signatures.py +++ b/lang/python/examples/howto/verify-signatures.py @@ -18,7 +18,7 @@ from __future__ import absolute_import, division, unicode_literals # 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 General Public License and the GNU -# Lesser General Public Licensefor more details. +# Lesser General Public License for more details. # # You should have received a copy of the GNU General Public License and the GNU # Lesser General Public along with this program; if not, see diff --git a/lang/python/examples/howto/verify-signed-file.py b/lang/python/examples/howto/verify-signed-file.py index 9f8702f..c561fe8 100755 --- a/lang/python/examples/howto/verify-signed-file.py +++ b/lang/python/examples/howto/verify-signed-file.py @@ -18,7 +18,7 @@ from __future__ import absolute_import, division, unicode_literals # 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 General Public License and the GNU -# Lesser General Public Licensefor more details. +# Lesser General Public License for more details. # # You should have received a copy of the GNU General Public License and the GNU # Lesser General Public along with this program; if not, see diff --git a/lang/python/examples/inter-edit.py b/lang/python/examples/inter-edit.py index ed0d8c4..5b58c97 100644 --- a/lang/python/examples/inter-edit.py +++ b/lang/python/examples/inter-edit.py @@ -15,15 +15,15 @@ # # You should have received a copy of the GNU General Public License # along with this program; if not, see <http://www.gnu.org/licenses/>. - """Simple interactive editor to test editor scripts""" from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import sys import gpg +del absolute_import, print_function, unicode_literals + if len(sys.argv) != 2: sys.exit("Usage: %s <Gpg key pattern>\n" % sys.argv[0]) @@ -40,10 +40,12 @@ with gpg.Context() as c: print("Editing key {} ({}):".format(key.uids[0].uid, key.subkeys[0].fpr)) def edit_fnc(keyword, args): - print("Status: {}, args: {} > ".format( - keyword, args), end='', flush=True) + print( + "Status: {}, args: {} > ".format(keyword, args), + end='', + flush=True) - if not 'GET' in keyword: + if 'GET' not in keyword: # no prompt print() return None diff --git a/lang/python/examples/low_level-encrypt_to_all.py b/lang/python/examples/low_level-encrypt_to_all.py index bad4220..5c10d3d 100755 --- a/lang/python/examples/low_level-encrypt_to_all.py +++ b/lang/python/examples/low_level-encrypt_to_all.py @@ -16,18 +16,18 @@ # # You should have received a copy of the GNU General Public License # along with this program; if not, see <http://www.gnu.org/licenses/>. - """ This program will try to encrypt a simple message to each key on your keyring. If your keyring has any invalid keys on it, those keys will be skipped and it will re-try the encryption.""" from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import sys import gpg +del absolute_import, print_function, unicode_literals + with gpg.Context(armor=True) as c: recipients = list() for key in c.keylist(): @@ -40,14 +40,15 @@ with gpg.Context(armor=True) as c: while not ciphertext: print("Encrypting to %d recipients" % len(recipients)) try: - ciphertext, _, _ = c.encrypt(b'This is my message.', - recipients=recipients) + ciphertext, _, _ = c.encrypt( + b'This is my message.', recipients=recipients) except gpg.errors.InvalidRecipients as e: print("Encryption failed for these keys:\n{0!s}".format(e)) # filter out the bad keys bad_keys = {bad.fpr for bad in e.recipients} - recipients = [r for r in recipients - if not r.subkeys[0].fpr in bad_keys] + recipients = [ + r for r in recipients if not r.subkeys[0].fpr in bad_keys + ] sys.stdout.buffer.write(ciphertext) diff --git a/lang/python/examples/sign.py b/lang/python/examples/sign.py index 16c2256..5b90b4b 100755 --- a/lang/python/examples/sign.py +++ b/lang/python/examples/sign.py @@ -17,12 +17,13 @@ # along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import sys import gpg from gpg.constants.sig import mode +del absolute_import, print_function, unicode_literals + with gpg.Context() as c: signed, _ = c.sign(b"Test message", mode=mode.CLEAR) sys.stdout.buffer.write(signed) diff --git a/lang/python/examples/signverify.py b/lang/python/examples/signverify.py index 5870ca9..2df7275 100755 --- a/lang/python/examples/signverify.py +++ b/lang/python/examples/signverify.py @@ -20,12 +20,13 @@ # It uses keys for joe+gpg@example.org generated by genkey.py script from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import sys import gpg from gpg.constants.sig import mode +del absolute_import, print_function, unicode_literals + user = "joe+gpg" with gpg.Context(pinentry_mode=gpg.constants.PINENTRY_MODE_LOOPBACK) as c: diff --git a/lang/python/examples/simple.py b/lang/python/examples/simple.py index 8f451d7..17c3eba 100755 --- a/lang/python/examples/simple.py +++ b/lang/python/examples/simple.py @@ -18,11 +18,12 @@ # along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import sys import gpg +del absolute_import, print_function, unicode_literals + with gpg.Context(armor=True) as c: recipients = [] print("Enter name of your recipient(s), end with a blank line.") @@ -40,8 +41,8 @@ with gpg.Context(armor=True) as c: if not recipients: sys.exit("No recipients.") - print("Encrypting for {}.".format(", ".join(k.uids[0].name - for k in recipients))) + print("Encrypting for {}.".format(", ".join( + k.uids[0].name for k in recipients))) ciphertext, _, _ = c.encrypt(b"This is my message,", recipients) sys.stdout.buffer.write(ciphertext) diff --git a/lang/python/examples/testCMSgetkey.py b/lang/python/examples/testCMSgetkey.py index d4c0884..f1cdb2c 100644 --- a/lang/python/examples/testCMSgetkey.py +++ b/lang/python/examples/testCMSgetkey.py @@ -15,15 +15,15 @@ # # You should have received a copy of the GNU General Public License # along with this program; if not, see <http://www.gnu.org/licenses/>. - """A test applicaton for the CMS protocol.""" from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import sys import gpg +del absolute_import, print_function, unicode_literals + if len(sys.argv) != 2: sys.exit("fingerprint or unique key ID for gpgme_get_key()") diff --git a/lang/python/examples/verifydetails.py b/lang/python/examples/verifydetails.py index b3ca133..dc0e7d3 100755 --- a/lang/python/examples/verifydetails.py +++ b/lang/python/examples/verifydetails.py @@ -18,11 +18,13 @@ # along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import sys import gpg +del absolute_import, print_function, unicode_literals + + def print_engine_infos(): print("gpgme version:", gpg.core.check_version(None)) print("engines:") @@ -31,8 +33,9 @@ def print_engine_infos(): print(engine.file_name, engine.version) for proto in [gpg.constants.protocol.OpenPGP, gpg.constants.protocol.CMS]: - print("Have {}? {}".format(gpg.core.get_protocol_name(proto), - gpg.core.engine_check_version(proto))) + print("Have {}? {}".format( + gpg.core.get_protocol_name(proto), + gpg.core.engine_check_version(proto))) def verifyprintdetails(filename, detached_sig_filename=None): @@ -40,9 +43,9 @@ def verifyprintdetails(filename, detached_sig_filename=None): with gpg.Context() as c: # Verify. - data, result = c.verify(open(filename), - open(detached_sig_filename) - if detached_sig_filename else None) + data, result = c.verify( + open(filename), + open(detached_sig_filename) if detached_sig_filename else None) # List results for all signatures. Status equal 0 means "Ok". for index, sign in enumerate(result.signatures): @@ -57,15 +60,15 @@ def verifyprintdetails(filename, detached_sig_filename=None): if data: sys.stdout.buffer.write(data) + def main(): print_engine_infos() print() argc = len(sys.argv) if argc < 2 or argc > 3: - sys.exit( - "Usage: {} <filename>[ <detached_signature_filename>]".format( - sys.argv[0])) + sys.exit("Usage: {} <filename>[ <detached_signature_filename>]".format( + sys.argv[0])) if argc == 2: print("trying to verify file {}.".format(sys.argv[1])) @@ -74,5 +77,6 @@ def main(): print("trying to verify signature {1} for file {0}.".format(*sys.argv)) verifyprintdetails(sys.argv[1], sys.argv[2]) + if __name__ == "__main__": main() diff --git a/lang/python/gpgme.i b/lang/python/gpgme.i index 492326b..f3d14a7 100644 --- a/lang/python/gpgme.i +++ b/lang/python/gpgme.i @@ -21,6 +21,12 @@ %include "cpointer.i" %include "cstring.i" +%{ +/* We use public symbols (eg. "_obsolete_class") which are marked as + * deprecated but we need to keep them. Silence the warning. */ +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +%} + /* Generate doc strings for all methods. This will generate docstrings of the form @@ -575,7 +581,8 @@ off_t gpgme_data_seek (gpgme_data_t dh, off_t offset, int whence=SEEK_SET); %} /* This is for notations, where we want to hide the length fields, and - the unused bit field block. */ + * the unused bit field block. We silence the warning. */ +%warnfilter(302) _gpgme_sig_notation; struct _gpgme_sig_notation { struct _gpgme_sig_notation *next; diff --git a/lang/python/setup.py.in b/lang/python/setup.py.in index 2595073..65a4be0 100755 --- a/lang/python/setup.py.in +++ b/lang/python/setup.py.in @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright (C) 2016-2017 g10 Code GmbH +# Copyright (C) 2016-2018 g10 Code GmbH # Copyright (C) 2004,2008 Igor Belyi <belyi@users.sourceforge.net> # Copyright (C) 2002 John Goerzen <jgoerzen@complete.org> # @@ -19,11 +19,15 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from distutils.core import setup, Extension -import os, os.path, sys +from distutils.command.build import build + import glob +import os +import os.path import re import shutil import subprocess +import sys # Out-of-tree build of the gpg bindings. gpg_error_config = ["gpg-error-config"] @@ -40,9 +44,11 @@ top_builddir = os.environ.get("top_builddir") if top_builddir: # In-tree build. in_tree = True - gpgme_config = [os.path.join(top_builddir, "src/gpgme-config")] + gpgme_config_flags + gpgme_config = [os.path.join(top_builddir, "src/gpgme-config") + ] + gpgme_config_flags gpgme_h = os.path.join(top_builddir, "src/gpgme.h") - library_dirs = [os.path.join(top_builddir, "src/.libs")] # XXX uses libtool internals + library_dirs = [os.path.join(top_builddir, + "src/.libs")] # XXX uses libtool internals extra_macros.update( HAVE_CONFIG_H=1, HAVE_DATA_H=1, @@ -55,17 +61,18 @@ else: devnull = open(os.devnull, "w") try: - subprocess.check_call(gpgme_config + ['--version'], - stdout=devnull) + subprocess.check_call(gpgme_config + ['--version'], stdout=devnull) except: sys.exit("Could not find gpgme-config. " + "Please install the libgpgme development package.") + def getconfig(what, config=gpgme_config): - confdata = subprocess.Popen(config + ["--%s" % what], - stdout=subprocess.PIPE).communicate()[0] + confdata = subprocess.Popen( + config + ["--%s" % what], stdout=subprocess.PIPE).communicate()[0] return [x for x in confdata.decode('utf-8').split() if x != ''] + version = version_raw = getconfig("version")[0] if '-' in version: version = version.split('-')[0] @@ -90,7 +97,7 @@ for item in getconfig('cflags'): include_dirs.append(item[2:]) elif item.startswith("-D"): defitem = item[2:].split("=", 1) - if len(defitem)==2: + if len(defitem) == 2: define_macros.append((defitem[0], defitem[1])) else: define_macros.append((defitem[0], None)) @@ -98,49 +105,67 @@ for item in getconfig('cflags'): # Adjust include and library locations in case of win32 uname_s = os.popen("uname -s").read() if uname_s.startswith("MINGW32"): - mnts = [x.split()[0:3:2] for x in os.popen("mount").read().split("\n") if x] - tmplist = sorted([(len(x[1]), x[1], x[0]) for x in mnts]) - tmplist.reverse() - extra_dirs = [] - for item in include_dirs: - for ln, mnt, tgt in tmplist: - if item.startswith(mnt): - item = os.path.normpath(item[ln:]) - while item[0] == os.path.sep: - item = item[1:] - extra_dirs.append(os.path.join(tgt, item)) - break - include_dirs += extra_dirs - for item in [x[2:] for x in libs if x.startswith("-L")]: - for ln, mnt, tgt in tmplist: - if item.startswith(mnt): - item = os.path.normpath(item[ln:]) - while item[0] == os.path.sep: - item = item[1:] - library_dirs.append(os.path.join(tgt, item)) - break + mnts = [ + x.split()[0:3:2] for x in os.popen("mount").read().split("\n") if x + ] + tmplist = sorted([(len(x[1]), x[1], x[0]) for x in mnts]) + tmplist.reverse() + extra_dirs = [] + for item in include_dirs: + for ln, mnt, tgt in tmplist: + if item.startswith(mnt): + item = os.path.normpath(item[ln:]) + while item[0] == os.path.sep: + item = item[1:] + extra_dirs.append(os.path.join(tgt, item)) + break + include_dirs += extra_dirs + for item in [x[2:] for x in libs if x.startswith("-L")]: + for ln, mnt, tgt in tmplist: + if item.startswith(mnt): + item = os.path.normpath(item[ln:]) + while item[0] == os.path.sep: + item = item[1:] + library_dirs.append(os.path.join(tgt, item)) + break + def in_srcdir(name): return os.path.join(os.environ.get("srcdir", ""), name) + + def up_to_date(source, target): - return (os.path.exists(target) - and os.path.getmtime(source) <= os.path.getmtime(target)) + return (os.path.exists(target) and + os.path.getmtime(source) <= os.path.getmtime(target)) + # We build an Extension using SWIG, which generates a Python module. # By default, the 'build_py' step is run before 'build_ext', and # therefore the generated Python module is not copied into the build # directory. -# Bug: http://bugs.python.org/issue1016626 +# Bugs: https://bugs.python.org/issue1016626 +# https://bugs.python.org/issue2624 # Workaround: -# http://stackoverflow.com/questions/12491328/python-distutils-not-include-the-swig-generated-module -from distutils.command.build import build -class BuildExtFirstHack(build): +# https://stackoverflow.com/questions/12491328/python-distutils-not-include-the-swig-generated-module +# +# To install to multiple Python installations or to alternate ones run the +# following three commands (yes, run the build one twice): +# +# /path/to/pythonX.Y setup.py build +# /path/to/pythonX.Y setup.py build +# /path/to/pythonX.Y setup.py install +# +# It is highly likely that this will need to be run as root or with sudo (or +# sudo -H). It may or may not work with venv. and outside a virtualenv +class BuildExtFirstHack(build): def _read_header(self, header, cflags): tmp_include = self._in_build_base("include1.h") with open(tmp_include, 'w') as f: f.write("#include <%s>" % header) - return subprocess.check_output(os.environ.get('CPP', 'cc -E').split() + cflags + [tmp_include]).decode('utf-8') + return subprocess.check_output( + os.environ.get('CPP', 'cc -E').split() + cflags + + [tmp_include]).decode('utf-8') def _write_if_unchanged(self, target, content): if os.path.exists(target): @@ -158,13 +183,14 @@ class BuildExtFirstHack(build): def _generate_errors_i(self): try: - subprocess.check_call(gpg_error_config + ['--version'], - stdout=devnull) + subprocess.check_call( + gpg_error_config + ['--version'], stdout=devnull) except: sys.exit("Could not find gpg-error-config. " + "Please install the libgpg-error development package.") - gpg_error_content = self._read_header("gpg-error.h", getconfig("cflags", config=gpg_error_config)) + gpg_error_content = self._read_header( + "gpg-error.h", getconfig("cflags", config=gpg_error_config)) filter_re = re.compile(r'GPG_ERR_[^ ]* =') rewrite_re = re.compile(r' *(.*) = .*') @@ -173,9 +199,11 @@ class BuildExtFirstHack(build): for line in gpg_error_content.splitlines(): if not filter_re.search(line): continue - errors_i_content += rewrite_re.sub(r'%constant long \1 = \1;'+'\n', line.strip()) + errors_i_content += rewrite_re.sub( + r'%constant long \1 = \1;' + '\n', line.strip()) - self._write_if_unchanged(self._in_build_base("errors.i"), errors_i_content) + self._write_if_unchanged( + self._in_build_base("errors.i"), errors_i_content) def _in_build_base(self, name): return os.path.join(self.build_base, name) @@ -191,7 +219,8 @@ class BuildExtFirstHack(build): # Copy due to http://bugs.python.org/issue2624 # Avoid creating in srcdir for source, target in ((in_srcdir(n), self._in_build_base(n)) - for n in ('gpgme.i', 'helpers.c', 'private.h', 'helpers.h')): + for n in ('gpgme.i', 'helpers.c', 'private.h', + 'helpers.h')): if not up_to_date(source, target): shutil.copy2(source, target) @@ -203,52 +232,60 @@ class BuildExtFirstHack(build): def run(self): self._generate() - swig_sources.extend((self._in_build_base('gpgme.i'), self._in_build_base('helpers.c'))) - swig_opts.extend(['-I' + self.build_base, - '-outdir', os.path.join(self.build_lib, 'gpg')]) + swig_sources.extend((self._in_build_base('gpgme.i'), + self._in_build_base('helpers.c'))) + swig_opts.extend([ + '-I' + self.build_base, '-outdir', + os.path.join(self.build_lib, 'gpg') + ]) include_dirs.insert(0, self.build_base) self.run_command('build_ext') build.run(self) + py3 = [] if sys.version_info.major < 3 else ['-py3'] swig_sources = [] swig_opts = ['-threads'] + py3 + extra_swig_opts -swige = Extension("gpg._gpgme", - sources = swig_sources, - swig_opts = swig_opts, - include_dirs = include_dirs, - define_macros = define_macros, - library_dirs = library_dirs, - extra_link_args = libs) - -setup(name="gpg", - cmdclass={'build': BuildExtFirstHack}, - version="@VERSION@", - description='Python bindings for GPGME GnuPG cryptography library', - # XXX add a long description - #long_description=long_description, - author='The GnuPG hackers', - author_email='gnupg-devel@gnupg.org', - url='https://www.gnupg.org', - ext_modules=[swige], - packages = ['gpg', 'gpg.constants', 'gpg.constants.data', - 'gpg.constants.keylist', 'gpg.constants.sig', - 'gpg.constants.tofu'], - license="LGPL2.1+ (the library), GPL2+ (tests and examples)", - classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Operating System :: POSIX', - 'Operating System :: Microsoft :: Windows', - 'Topic :: Communications :: Email', - 'Topic :: Security :: Cryptography', - ], +swige = Extension( + "gpg._gpgme", + sources=swig_sources, + swig_opts=swig_opts, + include_dirs=include_dirs, + define_macros=define_macros, + library_dirs=library_dirs, + extra_link_args=libs) + +setup( + name="gpg", + cmdclass={'build': BuildExtFirstHack}, + version="@VERSION@", + description='Python bindings for GPGME GnuPG cryptography library', + # TODO: add a long description + # long_description=long_description, + author='The GnuPG hackers', + author_email='gnupg-devel@gnupg.org', + url='https://www.gnupg.org', + ext_modules=[swige], + packages=[ + 'gpg', 'gpg.constants', 'gpg.constants.data', 'gpg.constants.keylist', + 'gpg.constants.sig', 'gpg.constants.tofu' + ], + license="LGPL2.1+ (the library), GPL2+ (tests and examples)", + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Operating System :: POSIX', + 'Operating System :: Microsoft :: Windows', + 'Topic :: Communications :: Email', + 'Topic :: Security :: Cryptography', + ], ) diff --git a/lang/python/src/__init__.py b/lang/python/src/__init__.py index 385b17e..30e638c 100644 --- a/lang/python/src/__init__.py +++ b/lang/python/src/__init__.py @@ -15,7 +15,6 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - """gpg: GnuPG Interface for Python (GPGME bindings) Welcome to gpg, the GnuPG Interface for Python. @@ -96,7 +95,6 @@ GPGME documentation: https://www.gnupg.org/documentation/manuals/gpgme/ """ from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from . import core from . import errors @@ -107,6 +105,8 @@ from . import version from .core import Context from .core import Data +del absolute_import, print_function, unicode_literals + # Interface hygiene. # Drop the low-level gpgme that creeps in for some reason. @@ -117,5 +117,7 @@ del gpgme _ = [Context, Data, core, errors, constants, util, callbacks, version] del _ -__all__ = ["Context", "Data", - "core", "errors", "constants", "util", "callbacks", "version"] +__all__ = [ + "Context", "Data", "core", "errors", "constants", "util", "callbacks", + "version" +] diff --git a/lang/python/src/callbacks.py b/lang/python/src/callbacks.py index b25a9a7..9aacf56 100644 --- a/lang/python/src/callbacks.py +++ b/lang/python/src/callbacks.py @@ -16,26 +16,30 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from getpass import getpass +del absolute_import, print_function, unicode_literals + + def passphrase_stdin(hint, desc, prev_bad, hook=None): """This is a sample callback that will read a passphrase from the terminal. The hook here, if present, will be used to describe why the passphrase is needed.""" why = '' - if hook != None: + if hook is not None: why = ' ' + hook if prev_bad: why += ' (again)' print("Please supply %s' password%s:" % (hint, why)) return getpass() + def progress_stdout(what, type, current, total, hook=None): - print("PROGRESS UPDATE: what = %s, type = %d, current = %d, total = %d" %\ + print("PROGRESS UPDATE: what = %s, type = %d, current = %d, total = %d" % (what, type, current, total)) + def readcb_fh(count, hook): """A callback for data. hook should be a Python file-like object.""" if count: diff --git a/lang/python/src/constants/__init__.py b/lang/python/src/constants/__init__.py index 484ffd2..7a953aa 100644 --- a/lang/python/src/constants/__init__.py +++ b/lang/python/src/constants/__init__.py @@ -18,23 +18,29 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from gpg import util +# Globals may need to be set prior to module import, if so this prevents PEP8 +# compliance, but better that than code breakage. util.process_constants('GPGME_', globals()) -del util # For convenience, we import the modules here. -from . import data, keylist, sig, tofu # The subdirs. -from . import create, event, keysign, md, pk, protocol, sigsum, status, validity +from . import data, keylist, sig, tofu # The subdirs. +# The remaining modules can no longer fit on one line. +from . import create, event, keysign, md, pk, protocol, sigsum, status +from . import validity + +del absolute_import, print_function, unicode_literals, util # A complication arises because 'import' is a reserved keyword. # Import it as 'Import' instead. -globals()['Import'] = getattr(__import__('', globals(), locals(), - [str('import')], 1), "import") +globals()['Import'] = getattr( + __import__('', globals(), locals(), [str('import')], 1), "import") -__all__ = ['data', 'event', 'import', 'keysign', 'keylist', 'md', 'pk', - 'protocol', 'sig', 'sigsum', 'status', 'tofu', 'validity', 'create'] +__all__ = [ + 'data', 'event', 'import', 'keysign', 'keylist', 'md', 'pk', 'protocol', + 'sig', 'sigsum', 'status', 'tofu', 'validity', 'create' +] # GPGME 1.7 replaced gpgme_op_edit with gpgme_op_interact. We # implement gpg.Context.op_edit using gpgme_op_interact, so the diff --git a/lang/python/src/constants/create.py b/lang/python/src/constants/create.py index 132e96d..382dad9 100644 --- a/lang/python/src/constants/create.py +++ b/lang/python/src/constants/create.py @@ -18,8 +18,7 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from gpg import util util.process_constants('GPGME_CREATE_', globals()) -del util +del absolute_import, print_function, unicode_literals, util diff --git a/lang/python/src/constants/data/__init__.py b/lang/python/src/constants/data/__init__.py index 8274ab9..c085667 100644 --- a/lang/python/src/constants/data/__init__.py +++ b/lang/python/src/constants/data/__init__.py @@ -1,6 +1,6 @@ - from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from . import encoding __all__ = ['encoding'] + +del absolute_import, print_function, unicode_literals diff --git a/lang/python/src/constants/data/encoding.py b/lang/python/src/constants/data/encoding.py index e76a22e..9afa732 100644 --- a/lang/python/src/constants/data/encoding.py +++ b/lang/python/src/constants/data/encoding.py @@ -16,8 +16,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from gpg import util util.process_constants('GPGME_DATA_ENCODING_', globals()) -del util +del absolute_import, print_function, unicode_literals, util diff --git a/lang/python/src/constants/event.py b/lang/python/src/constants/event.py index 1b14d1d..9f9273d 100644 --- a/lang/python/src/constants/event.py +++ b/lang/python/src/constants/event.py @@ -16,8 +16,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from gpg import util util.process_constants('GPGME_EVENT_', globals()) -del util +del absolute_import, print_function, unicode_literals, util diff --git a/lang/python/src/constants/import.py b/lang/python/src/constants/import.py index 47c296c..e477eb2 100644 --- a/lang/python/src/constants/import.py +++ b/lang/python/src/constants/import.py @@ -16,8 +16,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from gpg import util util.process_constants('GPGME_IMPORT_', globals()) -del util +del absolute_import, print_function, unicode_literals, util diff --git a/lang/python/src/constants/keylist/__init__.py b/lang/python/src/constants/keylist/__init__.py index 2ce0edf..fa8f7f0 100644 --- a/lang/python/src/constants/keylist/__init__.py +++ b/lang/python/src/constants/keylist/__init__.py @@ -1,6 +1,6 @@ - from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from . import mode __all__ = ['mode'] + +del absolute_import, print_function, unicode_literals diff --git a/lang/python/src/constants/keylist/mode.py b/lang/python/src/constants/keylist/mode.py index 39e1819..bda7710 100644 --- a/lang/python/src/constants/keylist/mode.py +++ b/lang/python/src/constants/keylist/mode.py @@ -16,8 +16,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from gpg import util util.process_constants('GPGME_KEYLIST_MODE_', globals()) -del util +del absolute_import, print_function, unicode_literals, util diff --git a/lang/python/src/constants/keysign.py b/lang/python/src/constants/keysign.py index fccdbc4..328dfb9 100644 --- a/lang/python/src/constants/keysign.py +++ b/lang/python/src/constants/keysign.py @@ -18,8 +18,7 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from gpg import util util.process_constants('GPGME_KEYSIGN_', globals()) -del util +del absolute_import, print_function, unicode_literals, util diff --git a/lang/python/src/constants/md.py b/lang/python/src/constants/md.py index f3e8bbd..068b31d 100644 --- a/lang/python/src/constants/md.py +++ b/lang/python/src/constants/md.py @@ -16,8 +16,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from gpg import util util.process_constants('GPGME_MD_', globals()) -del util +del absolute_import, print_function, unicode_literals, util diff --git a/lang/python/src/constants/pk.py b/lang/python/src/constants/pk.py index 6bf2a21..3a826d1 100644 --- a/lang/python/src/constants/pk.py +++ b/lang/python/src/constants/pk.py @@ -16,8 +16,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from gpg import util util.process_constants('GPGME_PK_', globals()) -del util +del absolute_import, print_function, unicode_literals, util diff --git a/lang/python/src/constants/protocol.py b/lang/python/src/constants/protocol.py index d086bbd..cc9ca07 100644 --- a/lang/python/src/constants/protocol.py +++ b/lang/python/src/constants/protocol.py @@ -16,8 +16,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from gpg import util util.process_constants('GPGME_PROTOCOL_', globals()) -del util +del absolute_import, print_function, unicode_literals, util diff --git a/lang/python/src/constants/sig/__init__.py b/lang/python/src/constants/sig/__init__.py index 39d4e6e..f45af00 100644 --- a/lang/python/src/constants/sig/__init__.py +++ b/lang/python/src/constants/sig/__init__.py @@ -1,6 +1,6 @@ - from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from . import mode, notation __all__ = ['mode', 'notation'] + +del absolute_import, print_function, unicode_literals diff --git a/lang/python/src/constants/sig/mode.py b/lang/python/src/constants/sig/mode.py index 0f4f0ef..3a2d17a 100644 --- a/lang/python/src/constants/sig/mode.py +++ b/lang/python/src/constants/sig/mode.py @@ -16,8 +16,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from gpg import util util.process_constants('GPGME_SIG_MODE_', globals()) -del util +del absolute_import, print_function, unicode_literals, util diff --git a/lang/python/src/constants/sig/notation.py b/lang/python/src/constants/sig/notation.py index 9a79e01..9e56be3 100644 --- a/lang/python/src/constants/sig/notation.py +++ b/lang/python/src/constants/sig/notation.py @@ -18,8 +18,7 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from gpg import util util.process_constants('GPGME_SIG_NOTATION_', globals()) -del util +del absolute_import, print_function, unicode_literals, util diff --git a/lang/python/src/constants/sigsum.py b/lang/python/src/constants/sigsum.py index 09ef9d7..0fe0e77 100644 --- a/lang/python/src/constants/sigsum.py +++ b/lang/python/src/constants/sigsum.py @@ -16,8 +16,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from gpg import util util.process_constants('GPGME_SIGSUM_', globals()) -del util +del absolute_import, print_function, unicode_literals, util diff --git a/lang/python/src/constants/tofu/__init__.py b/lang/python/src/constants/tofu/__init__.py index 819a58b..5e58a6a 100644 --- a/lang/python/src/constants/tofu/__init__.py +++ b/lang/python/src/constants/tofu/__init__.py @@ -18,7 +18,8 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from . import policy __all__ = ['policy'] + +del absolute_import, print_function, unicode_literals diff --git a/lang/python/src/constants/tofu/policy.py b/lang/python/src/constants/tofu/policy.py index 5a61f06..53d853d 100644 --- a/lang/python/src/constants/tofu/policy.py +++ b/lang/python/src/constants/tofu/policy.py @@ -18,8 +18,7 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from gpg import util util.process_constants('GPGME_TOFU_POLICY_', globals()) -del util +del absolute_import, print_function, unicode_literals, util diff --git a/lang/python/src/constants/validity.py b/lang/python/src/constants/validity.py index d3c5345..4ecf4ec 100644 --- a/lang/python/src/constants/validity.py +++ b/lang/python/src/constants/validity.py @@ -16,8 +16,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from gpg import util util.process_constants('GPGME_VALIDITY_', globals()) -del util +del absolute_import, print_function, unicode_literals, util diff --git a/lang/python/src/core.py b/lang/python/src/core.py index bd95d23..6e92592 100644 --- a/lang/python/src/core.py +++ b/lang/python/src/core.py @@ -1,5 +1,22 @@ -# Copyright (C) 2016-2017 g10 Code GmbH -# Copyright (C) 2004,2008 Igor Belyi <belyi@users.sourceforge.net> +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, print_function, unicode_literals + +import re +import os +import warnings +import weakref + +from . import gpgme +from .errors import errorcheck, GPGMEError +from . import constants +from . import errors +from . import util + +del absolute_import, print_function, unicode_literals + +# Copyright (C) 2016-2018 g10 Code GmbH +# Copyright (C) 2004, 2008 Igor Belyi <belyi@users.sourceforge.net> # Copyright (C) 2002 John Goerzen <jgoerzen@complete.org> # # This library is free software; you can redistribute it and/or @@ -15,7 +32,6 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - """Core functionality Core functionality of GPGME wrapped in a object-oriented fashion. @@ -24,18 +40,6 @@ and the 'Data' class describing buffers of data. """ -from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals - -import re -import os -import warnings -import weakref -from . import gpgme -from .errors import errorcheck, GPGMEError -from . import constants -from . import errors -from . import util class GpgmeWrapper(object): """Base wrapper class @@ -49,8 +53,8 @@ class GpgmeWrapper(object): self.wrapped = wrapped def __repr__(self): - return '<{}/{!r}>'.format(super(GpgmeWrapper, self).__repr__(), - self.wrapped) + return '<{}/{!r}>'.format( + super(GpgmeWrapper, self).__repr__(), self.wrapped) def __str__(self): acc = ['{}.{}'.format(__name__, self.__class__.__name__)] @@ -64,7 +68,7 @@ class GpgmeWrapper(object): return hash(repr(self.wrapped)) def __eq__(self, other): - if other == None: + if other is None: return False else: return repr(self.wrapped) == repr(other.wrapped) @@ -98,12 +102,12 @@ class GpgmeWrapper(object): _boolean_properties = set() def __wrap_boolean_property(self, key, do_set=False, value=None): - get_func = getattr(gpgme, - "{}get_{}".format(self._cprefix, key)) - set_func = getattr(gpgme, - "{}set_{}".format(self._cprefix, key)) + get_func = getattr(gpgme, "{}get_{}".format(self._cprefix, key)) + set_func = getattr(gpgme, "{}set_{}".format(self._cprefix, key)) + def get(slf): return bool(get_func(slf.wrapped)) + def set_(slf, value): set_func(slf.wrapped, bool(value)) @@ -116,9 +120,10 @@ class GpgmeWrapper(object): return get(self) _munge_docstring = re.compile(r'gpgme_([^(]*)\(([^,]*), (.*\) -> .*)') + def __getattr__(self, key): """On-the-fly generation of wrapper methods and properties""" - if key[0] == '_' or self._cprefix == None: + if key[0] == '_' or self._cprefix is None: return None if key in self._boolean_properties: @@ -128,12 +133,14 @@ class GpgmeWrapper(object): func = getattr(gpgme, name) if self._errorcheck(name): + def _funcwrap(slf, *args): result = func(slf.wrapped, *args) if slf._callback_excinfo: gpgme.gpg_raise_callback_exception(slf) return errorcheck(result, name) else: + def _funcwrap(slf, *args): result = func(slf.wrapped, *args) if slf._callback_excinfo: @@ -149,6 +156,7 @@ class GpgmeWrapper(object): # Bind the method to 'self'. def wrapper(*args): return _funcwrap(self, *args) + wrapper.__doc__ = doc return wrapper @@ -160,6 +168,7 @@ class GpgmeWrapper(object): else: super(GpgmeWrapper, self).__setattr__(key, value) + class Context(GpgmeWrapper): """Context for cryptographic operations @@ -173,10 +182,15 @@ class Context(GpgmeWrapper): """ - def __init__(self, armor=False, textmode=False, offline=False, - signers=[], pinentry_mode=constants.PINENTRY_MODE_DEFAULT, + def __init__(self, + armor=False, + textmode=False, + offline=False, + signers=[], + pinentry_mode=constants.PINENTRY_MODE_DEFAULT, protocol=constants.PROTOCOL_OpenPGP, - wrapped=None, home_dir=None): + wrapped=None, + home_dir=None): """Construct a context object Keyword arguments: @@ -212,22 +226,29 @@ class Context(GpgmeWrapper): Helper function to retrieve the results of an operation, or None if SINK is given. """ - if sink or data == None: + if sink or data is None: return None data.seek(0, os.SEEK_SET) return data.read() def __repr__(self): - return ( - "Context(armor={0.armor}, " - "textmode={0.textmode}, offline={0.offline}, " - "signers={0.signers}, pinentry_mode={0.pinentry_mode}, " - "protocol={0.protocol}, home_dir={0.home_dir}" - ")").format(self) - - def encrypt(self, plaintext, recipients=[], sign=True, sink=None, - passphrase=None, always_trust=False, add_encrypt_to=False, - prepare=False, expect_sign=False, compress=True): + return ("Context(armor={0.armor}, " + "textmode={0.textmode}, offline={0.offline}, " + "signers={0.signers}, pinentry_mode={0.pinentry_mode}, " + "protocol={0.protocol}, home_dir={0.home_dir}" + ")").format(self) + + def encrypt(self, + plaintext, + recipients=[], + sign=True, + sink=None, + passphrase=None, + always_trust=False, + add_encrypt_to=False, + prepare=False, + expect_sign=False, + compress=True): """Encrypt data Encrypt the given plaintext for the given recipients. If the @@ -267,12 +288,14 @@ class Context(GpgmeWrapper): flags |= expect_sign * constants.ENCRYPT_EXPECT_SIGN flags |= (not compress) * constants.ENCRYPT_NO_COMPRESS - if passphrase != None: + if passphrase is not None: old_pinentry_mode = self.pinentry_mode old_passphrase_cb = getattr(self, '_passphrase_cb', None) self.pinentry_mode = constants.PINENTRY_MODE_LOOPBACK + def passphrase_cb(hint, desc, prev_bad, hook=None): return passphrase + self.set_passphrase_cb(passphrase_cb) try: @@ -283,25 +306,26 @@ class Context(GpgmeWrapper): except errors.GPGMEError as e: result = self.op_encrypt_result() sig_result = self.op_sign_result() if sign else None - results = (self.__read__(sink, ciphertext), - result, sig_result) + results = (self.__read__(sink, ciphertext), result, sig_result) if e.getcode() == errors.UNUSABLE_PUBKEY: if result.invalid_recipients: - raise errors.InvalidRecipients(result.invalid_recipients, - error=e.error, - results=results) + raise errors.InvalidRecipients( + result.invalid_recipients, + error=e.error, + results=results) if e.getcode() == errors.UNUSABLE_SECKEY: sig_result = self.op_sign_result() if sig_result.invalid_signers: - raise errors.InvalidSigners(sig_result.invalid_signers, - error=e.error, - results=results) + raise errors.InvalidSigners( + sig_result.invalid_signers, + error=e.error, + results=results) # Otherwise, just raise the error, but attach the results # first. e.results = results raise e finally: - if passphrase != None: + if passphrase is not None: self.pinentry_mode = old_pinentry_mode if old_passphrase_cb: self.set_passphrase_cb(*old_passphrase_cb[1:]) @@ -342,37 +366,62 @@ class Context(GpgmeWrapper): GPGMEError -- as signaled by the underlying library """ + sink_result = None + verify_sigs = None plaintext = sink if sink else Data() - if passphrase != None: + if passphrase is not None: old_pinentry_mode = self.pinentry_mode old_passphrase_cb = getattr(self, '_passphrase_cb', None) self.pinentry_mode = constants.PINENTRY_MODE_LOOPBACK + def passphrase_cb(hint, desc, prev_bad, hook=None): return passphrase + self.set_passphrase_cb(passphrase_cb) try: - if verify: + if verify is not None: + if isinstance(verify, bool) is True: + if verify is False: + verify = True + sink_result = True + else: + pass + elif isinstance(verify, list) is True: + if len(verify) > 0: + verify_sigs = True + else: + pass + else: + verify = True self.op_decrypt_verify(ciphertext, plaintext) else: self.op_decrypt(ciphertext, plaintext) except errors.GPGMEError as e: result = self.op_decrypt_result() - verify_result = self.op_verify_result() if verify else None + if verify is not None and sink_result is None: + verify_result = self.op_verify_result() + else: + verify_result = None # Just raise the error, but attach the results first. - e.results = (self.__read__(sink, plaintext), - result, verify_result) + e.results = (self.__read__(sink, plaintext), result, verify_result) raise e finally: - if passphrase != None: + if passphrase is not None: self.pinentry_mode = old_pinentry_mode if old_passphrase_cb: self.set_passphrase_cb(*old_passphrase_cb[1:]) result = self.op_decrypt_result() - verify_result = self.op_verify_result() if verify else None + + if verify is not None and sink_result is None: + verify_result = self.op_verify_result() + else: + verify_result = None + results = (self.__read__(sink, plaintext), result, verify_result) + if result.unsupported_algorithm: raise errors.UnsupportedAlgorithm(result.unsupported_algorithm, results=results) @@ -382,8 +431,8 @@ class Context(GpgmeWrapper): for s in verify_result.signatures): raise errors.BadSignatures(verify_result, results=results) - if verify and verify != True: - missing = list() + if verify_sigs is not None: + missing = [] for key in verify: ok = False for subkey in key.subkeys: @@ -398,8 +447,29 @@ class Context(GpgmeWrapper): if not ok: missing.append(key) if missing: - raise errors.MissingSignatures(verify_result, missing, - results=results) + try: + raise errors.MissingSignatures(verify_result, missing, + results=results) + except errors.MissingSignatures as e: + raise e + # mse = e + # mserr = "gpg.errors.MissingSignatures:" + # print(mserr, miss_e, "\n") + # # The full details can then be found in mse.results, + # # mse.result, mse.missing if necessary. + # mse_list = [] + # msp = "Missing signatures from: \n".format() + # print(msp) + # for key in mse.missing: + # mse_list.append(key.fpr) + # msl = [] + # msl.append(key.fpr) + # for user in key.uids: + # msl.append(user.name) + # msl.append(user.email) + # # msl.append(user.uid) + # print(" ".join(msl)) + # raise mse return results @@ -431,13 +501,13 @@ class Context(GpgmeWrapper): try: self.op_sign(data, signeddata, mode) except errors.GPGMEError as e: - results = (self.__read__(sink, signeddata), - self.op_sign_result()) + results = (self.__read__(sink, signeddata), self.op_sign_result()) if e.getcode() == errors.UNUSABLE_SECKEY: if results[1].invalid_signers: - raise errors.InvalidSigners(results[1].invalid_signers, - error=e.error, - results=results) + raise errors.InvalidSigners( + results[1].invalid_signers, + error=e.error, + results=results) e.results = results raise e @@ -481,8 +551,7 @@ class Context(GpgmeWrapper): self.op_verify(signed_data, None, data) except errors.GPGMEError as e: # Just raise the error, but attach the results first. - e.results = (self.__read__(sink, data), - self.op_verify_result()) + e.results = (self.__read__(sink, data), self.op_verify_result()) raise e results = (self.__read__(sink, data), self.op_verify_result()) @@ -504,12 +573,171 @@ class Context(GpgmeWrapper): if not ok: missing.append(key) if missing: - raise errors.MissingSignatures(results[1], missing, - results=results) + raise errors.MissingSignatures( + results[1], missing, results=results) return results - def keylist(self, pattern=None, secret=False, + def key_import(self, data): + """Import data + + Imports the given data into the Context. + + Returns: + -- an object describing the results of imported or updated + keys + + Raises: + TypeError -- Very rarely. + GPGMEError -- as signaled by the underlying library: + + Import status errors, when they occur, will usually + be of NODATA. NO_PUBKEY indicates something + managed to run the function without any + arguments, while an argument of None triggers + the first NODATA of errors.GPGME in the + exception. + """ + try: + self.op_import(data) + result = self.op_import_result() + if result.considered == 0: + status = constants.STATUS_IMPORT_PROBLEM + else: + status = constants.STATUS_KEY_CONSIDERED + except Exception as e: + if e == errors.GPGMEError: + if e.code_str == "No data": + status = constants.STATUS_NODATA + else: + status = constants.STATUS_FILE_ERROR + elif e == TypeError and hasattr(data, "decode") is True: + status = constants.STATUS_NO_PUBKEY + elif e == TypeError and hasattr(data, "encode") is True: + status = constants.STATUS_FILE_ERROR + else: + status = constants.STATUS_ERROR + + if status == constants.STATUS_KEY_CONSIDERED: + import_result = result + else: + import_result = status + + return import_result + + def key_export(self, pattern=None): + """Export keys. + + Exports public keys matching the pattern specified. If no + pattern is specified then exports all available keys. + + Keyword arguments: + pattern -- return keys matching pattern (default: all keys) + + Returns: + -- A key block containing one or more OpenPGP keys in + either ASCII armoured or binary format as determined + by the Context(). If there are no matching keys it + returns None. + + Raises: + GPGMEError -- as signaled by the underlying library. + """ + data = Data() + mode = 0 + try: + self.op_export(pattern, mode, data) + data.seek(0, os.SEEK_SET) + pk_result = data.read() + except GPGMEError as e: + pk_result = e + + if len(pk_result) > 0: + result = pk_result + else: + result = None + + return result + + def key_export_minimal(self, pattern=None): + """Export keys. + + Exports public keys matching the pattern specified in a + minimised format. If no pattern is specified then exports all + available keys. + + Keyword arguments: + pattern -- return keys matching pattern (default: all keys) + + Returns: + -- A key block containing one or more minimised OpenPGP + keys in either ASCII armoured or binary format as + determined by the Context(). If there are no matching + keys it returns None. + + Raises: + GPGMEError -- as signaled by the underlying library. + """ + data = Data() + mode = gpgme.GPGME_EXPORT_MODE_MINIMAL + try: + self.op_export(pattern, mode, data) + data.seek(0, os.SEEK_SET) + pk_result = data.read() + except GPGMEError as e: + pk_result = e + + if len(pk_result) > 0: + result = pk_result + else: + result = None + + return result + + def key_export_secret(self, pattern=None): + """Export secret keys. + + Exports secret keys matching the pattern specified. If no + pattern is specified then exports or attempts to export all + available secret keys. + + IMPORTANT: Each secret key to be exported will prompt for its + passphrase via an invocation of pinentry and gpg-agent. If the + passphrase is not entered or does not match then no data will be + exported. This is the same result as when specifying a pattern + that is not matched by the available keys. + + Keyword arguments: + pattern -- return keys matching pattern (default: all keys) + + Returns: + -- On success a key block containing one or more OpenPGP + secret keys in either ASCII armoured or binary format + as determined by the Context(). + -- On failure while not raising an exception, returns None. + + Raises: + GPGMEError -- as signaled by the underlying library. + """ + data = Data() + mode = gpgme.GPGME_EXPORT_MODE_SECRET + try: + self.op_export(pattern, mode, data) + data.seek(0, os.SEEK_SET) + sk_result = data.read() + except GPGMEError as e: + sk_result = e + + if len(sk_result) > 0: + result = sk_result + else: + result = None + + return result + + def keylist(self, + pattern=None, + secret=False, mode=constants.keylist.mode.LOCAL, source=None): """List keys @@ -544,9 +772,17 @@ class Context(GpgmeWrapper): key = self.op_keylist_next() self.op_keylist_end() - def create_key(self, userid, algorithm=None, expires_in=0, expires=True, - sign=False, encrypt=False, certify=False, authenticate=False, - passphrase=None, force=False): + def create_key(self, + userid, + algorithm=None, + expires_in=0, + expires=True, + sign=False, + encrypt=False, + certify=False, + authenticate=False, + passphrase=None, + force=False): """Create a primary key Create a primary key for the user id USERID. @@ -583,9 +819,10 @@ class Context(GpgmeWrapper): encrypt -- request the encryption capability (see above) certify -- request the certification capability (see above) authenticate -- request the authentication capability (see above) - passphrase -- protect the key with a passphrase (default: no passphrase) - force -- force key creation even if a key with the same userid exists - (default: False) + passphrase -- protect the key with a passphrase (default: no + passphrase) + force -- force key creation even if a key with the same userid + exists (default: False) Returns: -- an object describing the result of the key creation @@ -598,22 +835,26 @@ class Context(GpgmeWrapper): old_pinentry_mode = self.pinentry_mode old_passphrase_cb = getattr(self, '_passphrase_cb', None) self.pinentry_mode = constants.PINENTRY_MODE_LOOPBACK + def passphrase_cb(hint, desc, prev_bad, hook=None): return passphrase + self.set_passphrase_cb(passphrase_cb) try: - self.op_createkey(userid, algorithm, - 0, # reserved - expires_in, - None, # extrakey - ((constants.create.SIGN if sign else 0) - | (constants.create.ENCR if encrypt else 0) - | (constants.create.CERT if certify else 0) - | (constants.create.AUTH if authenticate else 0) - | (constants.create.NOPASSWD if passphrase == None else 0) - | (0 if expires else constants.create.NOEXPIRE) - | (constants.create.FORCE if force else 0))) + self.op_createkey( + userid, + algorithm, + 0, # reserved + expires_in, + None, # extrakey + ((constants.create.SIGN if sign else 0) | + (constants.create.ENCR if encrypt else 0) | + (constants.create.CERT if certify else 0) | + (constants.create.AUTH if authenticate else 0) | + (constants.create.NOPASSWD if passphrase is None else 0) | + (0 if expires else constants.create.NOEXPIRE) | + (constants.create.FORCE if force else 0))) finally: if util.is_a_string(passphrase): self.pinentry_mode = old_pinentry_mode @@ -622,8 +863,15 @@ class Context(GpgmeWrapper): return self.op_genkey_result() - def create_subkey(self, key, algorithm=None, expires_in=0, expires=True, - sign=False, encrypt=False, authenticate=False, passphrase=None): + def create_subkey(self, + key, + algorithm=None, + expires_in=0, + expires=True, + sign=False, + encrypt=False, + authenticate=False, + passphrase=None): """Create a subkey Create a subkey for the given KEY. As subkeys are a concept @@ -659,7 +907,8 @@ class Context(GpgmeWrapper): sign -- request the signing capability (see above) encrypt -- request the encryption capability (see above) authenticate -- request the authentication capability (see above) - passphrase -- protect the subkey with a passphrase (default: no passphrase) + passphrase -- protect the subkey with a passphrase (default: no + passphrase) Returns: -- an object describing the result of the subkey creation @@ -672,20 +921,23 @@ class Context(GpgmeWrapper): old_pinentry_mode = self.pinentry_mode old_passphrase_cb = getattr(self, '_passphrase_cb', None) self.pinentry_mode = constants.PINENTRY_MODE_LOOPBACK + def passphrase_cb(hint, desc, prev_bad, hook=None): return passphrase + self.set_passphrase_cb(passphrase_cb) try: - self.op_createsubkey(key, algorithm, - 0, # reserved - expires_in, - ((constants.create.SIGN if sign else 0) - | (constants.create.ENCR if encrypt else 0) - | (constants.create.AUTH if authenticate else 0) - | (constants.create.NOPASSWD - if passphrase == None else 0) - | (0 if expires else constants.create.NOEXPIRE))) + self.op_createsubkey( + key, + algorithm, + 0, # reserved + expires_in, + ((constants.create.SIGN if sign else 0) | + (constants.create.ENCR if encrypt else 0) | + (constants.create.AUTH if authenticate else 0) | + (constants.create.NOPASSWD if passphrase is None else 0) | + (0 if expires else constants.create.NOEXPIRE))) finally: if util.is_a_string(passphrase): self.pinentry_mode = old_pinentry_mode @@ -745,8 +997,8 @@ class Context(GpgmeWrapper): """ flags = 0 - if uids == None or util.is_a_string(uids): - pass#through unchanged + if uids is None or util.is_a_string(uids): + pass # through unchanged else: flags |= constants.keysign.LFSEP uids = "\n".join(uids) @@ -771,8 +1023,11 @@ class Context(GpgmeWrapper): """ self.op_tofu_policy(key, policy) - def assuan_transact(self, command, - data_cb=None, inquire_cb=None, status_cb=None): + def assuan_transact(self, + command, + data_cb=None, + inquire_cb=None, + status_cb=None): """Issue a raw assuan command This function can be used to issue a raw assuan command to the @@ -803,12 +1058,10 @@ class Context(GpgmeWrapper): errptr = gpgme.new_gpgme_error_t_p() err = gpgme.gpgme_op_assuan_transact_ext( - self.wrapped, - cmd, - (weakref.ref(self), data_cb) if data_cb else None, - (weakref.ref(self), inquire_cb) if inquire_cb else None, - (weakref.ref(self), status_cb) if status_cb else None, - errptr) + self.wrapped, cmd, (weakref.ref(self), data_cb) + if data_cb else None, (weakref.ref(self), inquire_cb) + if inquire_cb else None, (weakref.ref(self), status_cb) + if status_cb else None, errptr) if self._callback_excinfo: gpgme.gpg_raise_callback_exception(self) @@ -836,10 +1089,10 @@ class Context(GpgmeWrapper): GPGMEError -- as signaled by the underlying library """ - if key == None: + if key is None: raise ValueError("First argument cannot be None") - if sink == None: + if sink is None: sink = Data() if fnc_value: @@ -847,8 +1100,8 @@ class Context(GpgmeWrapper): else: opaquedata = (weakref.ref(self), func) - result = gpgme.gpgme_op_interact(self.wrapped, key, flags, - opaquedata, sink) + result = gpgme.gpgme_op_interact(self.wrapped, key, flags, opaquedata, + sink) if self._callback_excinfo: gpgme.gpg_raise_callback_exception(self) errorcheck(result) @@ -857,6 +1110,7 @@ class Context(GpgmeWrapper): def signers(self): """Keys used for signing""" return [self.signers_enum(i) for i in range(self.signers_count())] + @signers.setter def signers(self, signers): old = self.signers @@ -872,6 +1126,7 @@ class Context(GpgmeWrapper): def pinentry_mode(self): """Pinentry mode""" return self.get_pinentry_mode() + @pinentry_mode.setter def pinentry_mode(self, value): self.set_pinentry_mode(value) @@ -880,6 +1135,7 @@ class Context(GpgmeWrapper): def protocol(self): """Protocol to use""" return self.get_protocol() + @protocol.setter def protocol(self, value): errorcheck(gpgme.gpgme_engine_check_version(value)) @@ -889,6 +1145,7 @@ class Context(GpgmeWrapper): def home_dir(self): """Engine's home directory""" return self.engine_info.home_dir + @home_dir.setter def home_dir(self, value): self.set_engine_info(self.protocol, home_dir=value) @@ -901,24 +1158,16 @@ class Context(GpgmeWrapper): # The list of functions is created using: # # $ grep '^gpgme_error_t ' obj/lang/python/python3.5-gpg/gpgme.h \ - # | grep -v _op_ | awk "/\(gpgme_ctx/ { printf (\"'%s',\\n\", \$2) } " - return ((name.startswith('gpgme_op_') - and not name.endswith('_result')) - or name in { - 'gpgme_new', - 'gpgme_set_ctx_flag', - 'gpgme_set_protocol', - 'gpgme_set_sub_protocol', - 'gpgme_set_keylist_mode', - 'gpgme_set_pinentry_mode', - 'gpgme_set_locale', - 'gpgme_ctx_set_engine_info', - 'gpgme_signers_add', - 'gpgme_sig_notation_add', - 'gpgme_set_sender', - 'gpgme_cancel', - 'gpgme_cancel_async', - 'gpgme_get_key', + # | grep -v _op_ | awk "/\(gpgme_ctx/ { printf (\"'%s',\\n\", \$2) } " + return ((name.startswith('gpgme_op_') and not + name.endswith('_result')) or name in { + 'gpgme_new', 'gpgme_set_ctx_flag', 'gpgme_set_protocol', + 'gpgme_set_sub_protocol', 'gpgme_set_keylist_mode', + 'gpgme_set_pinentry_mode', 'gpgme_set_locale', + 'gpgme_ctx_set_engine_info', 'gpgme_signers_add', + 'gpgme_sig_notation_add', 'gpgme_set_sender', + 'gpgme_cancel', 'gpgme_cancel_async', 'gpgme_get_key', + 'gpgme_get_sig_key', }) _boolean_properties = {'armor', 'textmode', 'offline'} @@ -938,6 +1187,7 @@ class Context(GpgmeWrapper): # Implement the context manager protocol. def __enter__(self): return self + def __exit__(self, type, value, tb): self.__del__() @@ -1032,10 +1282,10 @@ class Context(GpgmeWrapper): Please see the GPGME manual for more information. """ - if func == None: + if func is None: hookdata = None else: - if hook == None: + if hook is None: hookdata = (weakref.ref(self), func) else: hookdata = (weakref.ref(self), func, hook) @@ -1057,10 +1307,10 @@ class Context(GpgmeWrapper): Please see the GPGME manual for more information. """ - if func == None: + if func is None: hookdata = None else: - if hook == None: + if hook is None: hookdata = (weakref.ref(self), func) else: hookdata = (weakref.ref(self), func, hook) @@ -1081,10 +1331,10 @@ class Context(GpgmeWrapper): Please see the GPGME manual for more information. """ - if func == None: + if func is None: hookdata = None else: - if hook == None: + if hook is None: hookdata = (weakref.ref(self), func) else: hookdata = (weakref.ref(self), func, hook) @@ -1152,8 +1402,8 @@ class Context(GpgmeWrapper): magic numbers will break as a result. """ - warnings.warn("Call to deprecated method op_edit.", - category=DeprecationWarning) + warnings.warn( + "Call to deprecated method op_edit.", category=DeprecationWarning) return self.interact(key, func, sink=out, fnc_value=fnc_value) @@ -1182,7 +1432,8 @@ class Data(GpgmeWrapper): # This list is compiled using # # $ grep -v '^gpgme_error_t ' obj/lang/python/python3.5-gpg/gpgme.h \ - # | awk "/\(gpgme_data_t/ { printf (\"'%s',\\n\", \$2) } " | sed "s/'\\*/'/" + # | awk "/\(gpgme_data_t/ { printf (\"'%s',\\n\", \$2) } " \ + # | sed "s/'\\*/'/" return name not in { 'gpgme_data_read', 'gpgme_data_write', @@ -1191,11 +1442,17 @@ class Data(GpgmeWrapper): 'gpgme_data_release_and_get_mem', 'gpgme_data_get_encoding', 'gpgme_data_get_file_name', + 'gpgme_data_set_flag', 'gpgme_data_identify', } - def __init__(self, string=None, file=None, offset=None, - length=None, cbs=None, copy=True): + def __init__(self, + string=None, + file=None, + offset=None, + length=None, + cbs=None, + copy=True): """Initialize a new gpgme_data_t object. If no args are specified, make it an empty object. @@ -1239,13 +1496,13 @@ class Data(GpgmeWrapper): super(Data, self).__init__(None) self.data_cbs = None - if cbs != None: + if cbs is not None: self.new_from_cbs(*cbs) - elif string != None: + elif string is not None: self.new_from_mem(string, copy) - elif file != None and offset != None and length != None: + elif file is not None and offset is not None and length is not None: self.new_from_filepart(file, offset, length) - elif file != None: + elif file is not None: if util.is_a_string(file): self.new_from_file(file, copy) else: @@ -1258,7 +1515,7 @@ class Data(GpgmeWrapper): # At interpreter shutdown, gpgme is set to NONE. return - if self.wrapped != None and gpgme.gpgme_data_release: + if self.wrapped is not None and gpgme.gpgme_data_release: gpgme.gpgme_data_release(self.wrapped) if self._callback_excinfo: gpgme.gpg_raise_callback_exception(self) @@ -1268,6 +1525,7 @@ class Data(GpgmeWrapper): # Implement the context manager protocol. def __enter__(self): return self + def __exit__(self, type, value, tb): self.__del__() @@ -1282,7 +1540,8 @@ class Data(GpgmeWrapper): def new_from_mem(self, string, copy=True): tmp = gpgme.new_gpgme_data_t_p() - errorcheck(gpgme.gpgme_data_new_from_mem(tmp,string,len(string),copy)) + errorcheck( + gpgme.gpgme_data_new_from_mem(tmp, string, len(string), copy)) self.wrapped = gpgme.gpgme_data_t_p_value(tmp) gpgme.delete_gpgme_data_t_p(tmp) @@ -1300,12 +1559,12 @@ class Data(GpgmeWrapper): def new_from_cbs(self, read_cb, write_cb, seek_cb, release_cb, hook=None): tmp = gpgme.new_gpgme_data_t_p() - if hook != None: - hookdata = (weakref.ref(self), - read_cb, write_cb, seek_cb, release_cb, hook) + if hook is not None: + hookdata = (weakref.ref(self), read_cb, write_cb, seek_cb, + release_cb, hook) else: - hookdata = (weakref.ref(self), - read_cb, write_cb, seek_cb, release_cb) + hookdata = (weakref.ref(self), read_cb, write_cb, seek_cb, + release_cb) gpgme.gpg_data_new_from_cbs(self, hookdata, tmp) self.wrapped = gpgme.gpgme_data_t_p_value(tmp) gpgme.delete_gpgme_data_t_p(tmp) @@ -1327,12 +1586,13 @@ class Data(GpgmeWrapper): filename = file else: fp = gpgme.fdopen(file.fileno(), file.mode) - if fp == None: - raise ValueError("Failed to open file from %s arg %s" % \ - (str(type(file)), str(file))) + if fp is None: + raise ValueError("Failed to open file from %s arg %s" % (str( + type(file)), str(file))) - errorcheck(gpgme.gpgme_data_new_from_filepart(tmp, filename, fp, - offset, length)) + errorcheck( + gpgme.gpgme_data_new_from_filepart(tmp, filename, fp, offset, + length)) self.wrapped = gpgme.gpgme_data_t_p_value(tmp) gpgme.delete_gpgme_data_t_p(tmp) @@ -1349,10 +1609,16 @@ class Data(GpgmeWrapper): def new_from_stream(self, file): """This wrap around gpgme_data_new_from_stream is an alias for - new_from_fd() method since in python there's not difference - between file stream and file descriptor""" + new_from_fd() method since in python there's no difference + between file stream and file descriptor.""" self.new_from_fd(file) + def new_from_estream(self, file): + """This wrap around gpgme_data_new_from_estream is an alias for + new_from_fd() method since in python there's no difference + between file stream and file descriptor, but using fd broke.""" + self.new_from_stream(file) + def write(self, buffer): """Write buffer given as string or bytes. @@ -1365,7 +1631,7 @@ class Data(GpgmeWrapper): raise GPGMEError.fromSyserror() return written - def read(self, size = -1): + def read(self, size=-1): """Read at most size bytes, returned as bytes. If the size argument is negative or omitted, read until EOF is reached. @@ -1400,6 +1666,7 @@ class Data(GpgmeWrapper): chunks.append(result) return b''.join(chunks) + def pubkey_algo_string(subkey): """Return short algorithm string @@ -1412,6 +1679,7 @@ def pubkey_algo_string(subkey): """ return gpgme.gpgme_pubkey_algo_string(subkey) + def pubkey_algo_name(algo): """Return name of public key algorithm @@ -1424,6 +1692,7 @@ def pubkey_algo_name(algo): """ return gpgme.gpgme_pubkey_algo_name(algo) + def hash_algo_name(algo): """Return name of hash algorithm @@ -1436,6 +1705,7 @@ def hash_algo_name(algo): """ return gpgme.gpgme_hash_algo_name(algo) + def get_protocol_name(proto): """Get protocol description @@ -1447,6 +1717,7 @@ def get_protocol_name(proto): """ return gpgme.gpgme_get_protocol_name(proto) + def addrspec_from_uid(uid): """Return the address spec @@ -1458,22 +1729,26 @@ def addrspec_from_uid(uid): """ return gpgme.gpgme_addrspec_from_uid(uid) + def check_version(version=None): return gpgme.gpgme_check_version(version) + # check_version also makes sure that several subsystems are properly # initialized, and it must be run at least once before invoking any # other function. We do it here so that the user does not have to do # it unless she really wants to check for a certain version. check_version() -def engine_check_version (proto): + +def engine_check_version(proto): try: errorcheck(gpgme.gpgme_engine_check_version(proto)) return True except errors.GPGMEError: return False + def get_engine_info(): ptr = gpgme.new_gpgme_engine_info_t_p() try: @@ -1484,6 +1759,7 @@ def get_engine_info(): gpgme.delete_gpgme_engine_info_t_p(ptr) return info + def set_engine_info(proto, file_name, home_dir=None): """Changes the default configuration of the crypto engine implementing the protocol 'proto'. 'file_name' is the file name of @@ -1492,10 +1768,12 @@ def set_engine_info(proto, file_name, home_dir=None): used if omitted).""" errorcheck(gpgme.gpgme_set_engine_info(proto, file_name, home_dir)) + def set_locale(category, value): """Sets the default locale used by contexts""" errorcheck(gpgme.gpgme_set_locale(None, category, value)) + def wait(hang): """Wait for asynchronous call on any Context to finish. Wait forever if hang is True. @@ -1509,7 +1787,7 @@ def wait(hang): context = gpgme.gpgme_wait(None, ptr, hang) status = gpgme.gpgme_error_t_p_value(ptr) gpgme.delete_gpgme_error_t_p(ptr) - if context == None: + if context is None: errorcheck(status) else: context = Context(context) diff --git a/lang/python/src/errors.py b/lang/python/src/errors.py index c41ac69..9c7f037 100644 --- a/lang/python/src/errors.py +++ b/lang/python/src/errors.py @@ -17,11 +17,12 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals from . import gpgme from . import util +del absolute_import, print_function, unicode_literals + # To appease static analysis tools, we define some constants here. # They are overwritten with the proper values by process_constants. NO_ERROR = None @@ -30,6 +31,7 @@ EOF = None util.process_constants('GPG_ERR_', globals()) del util + class GpgError(Exception): """A GPG Error @@ -55,6 +57,7 @@ class GpgError(Exception): exception objects. """ + def __init__(self, error=None, context=None, results=None): self.error = error self.context = context @@ -62,37 +65,38 @@ class GpgError(Exception): @property def code(self): - if self.error == None: + if self.error is None: return None return gpgme.gpgme_err_code(self.error) @property def code_str(self): - if self.error == None: + if self.error is None: return None return gpgme.gpgme_strerror(self.error) @property def source(self): - if self.error == None: + if self.error is None: return None return gpgme.gpgme_err_source(self.error) @property def source_str(self): - if self.error == None: + if self.error is None: return None return gpgme.gpgme_strsource(self.error) def __str__(self): msgs = [] - if self.context != None: + if self.context is not None: msgs.append(self.context) - if self.error != None: + if self.error is not None: msgs.append(self.source_str) msgs.append(self.code_str) return ': '.join(msgs) + class GPGMEError(GpgError): '''Generic error @@ -101,24 +105,30 @@ class GPGMEError(GpgError): returns an error. This is the error that was used in PyME. ''' + @classmethod def fromSyserror(cls): return cls(gpgme.gpgme_err_code_from_syserror()) + @property def message(self): return self.context + def getstring(self): return str(self) + def getcode(self): return self.code + def getsource(self): return self.source -def errorcheck(retval, extradata = None): +def errorcheck(retval, extradata=None): if retval: raise GPGMEError(retval, extradata) + class KeyNotFound(GPGMEError, KeyError): """Raised if a key was not found @@ -127,63 +137,76 @@ class KeyNotFound(GPGMEError, KeyError): indicating EOF, and a KeyError. """ + def __init__(self, keystr): self.keystr = keystr GPGMEError.__init__(self, EOF) + def __str__(self): return self.keystr + # These errors are raised in the idiomatic interface code. + class EncryptionError(GpgError): pass + class InvalidRecipients(EncryptionError): def __init__(self, recipients, **kwargs): EncryptionError.__init__(self, **kwargs) self.recipients = recipients + def __str__(self): - return ", ".join("{}: {}".format(r.fpr, - gpgme.gpgme_strerror(r.reason)) + return ", ".join("{}: {}".format(r.fpr, gpgme.gpgme_strerror(r.reason)) for r in self.recipients) + class DeryptionError(GpgError): pass + class UnsupportedAlgorithm(DeryptionError): def __init__(self, algorithm, **kwargs): DeryptionError.__init__(self, **kwargs) self.algorithm = algorithm + def __str__(self): return self.algorithm + class SigningError(GpgError): pass + class InvalidSigners(SigningError): def __init__(self, signers, **kwargs): SigningError.__init__(self, **kwargs) self.signers = signers + def __str__(self): - return ", ".join("{}: {}".format(s.fpr, - gpgme.gpgme_strerror(s.reason)) + return ", ".join("{}: {}".format(s.fpr, gpgme.gpgme_strerror(s.reason)) for s in self.signers) + class VerificationError(GpgError): def __init__(self, result, **kwargs): GpgError.__init__(self, **kwargs) self.result = result + class BadSignatures(VerificationError): def __str__(self): - return ", ".join("{}: {}".format(s.fpr, - gpgme.gpgme_strerror(s.status)) + return ", ".join("{}: {}".format(s.fpr, gpgme.gpgme_strerror(s.status)) for s in self.result.signatures if s.status != NO_ERROR) + class MissingSignatures(VerificationError): def __init__(self, result, missing, **kwargs): VerificationError.__init__(self, result, **kwargs) self.missing = missing + def __str__(self): return ", ".join(k.subkeys[0].fpr for k in self.missing) diff --git a/lang/python/src/results.py b/lang/python/src/results.py index bfd0f68..6b5f63c 100644 --- a/lang/python/src/results.py +++ b/lang/python/src/results.py @@ -19,7 +19,6 @@ from __future__ import absolute_import, print_function, unicode_literals del absolute_import, print_function, unicode_literals - """Robust result objects Results returned by the underlying library are fragile, i.e. they are @@ -30,23 +29,28 @@ therefore create deep copies of the results. """ + class Result(object): """Result object Describes the result of an operation. """ - """Convert to types""" _type = {} - """Map functions over list attributes""" _map = {} - """Automatically copy unless blacklisted""" _blacklist = { - 'acquire', 'append', 'disown', 'next', 'own', 'this', 'thisown', + 'acquire', + 'append', + 'disown', + 'next', + 'own', + 'this', + 'thisown', } + def __init__(self, fragile): for key, func in self._type.items(): if hasattr(fragile, key): @@ -67,52 +71,67 @@ class Result(object): def __repr__(self): return '{}({})'.format( self.__class__.__name__, - ', '.join('{}={!r}'.format(k, getattr(self, k)) - for k in dir(self) if not k.startswith('_'))) + ', '.join('{}={!r}'.format(k, getattr(self, k)) for k in dir(self) + if not k.startswith('_'))) + class InvalidKey(Result): pass + class EncryptResult(Result): _map = dict(invalid_recipients=InvalidKey) + class Recipient(Result): pass + class DecryptResult(Result): _type = dict(wrong_key_usage=bool, is_de_vs=bool) _map = dict(recipients=Recipient) + class NewSignature(Result): pass + class SignResult(Result): _map = dict(invalid_signers=InvalidKey, signatures=NewSignature) + class Notation(Result): pass + class Signature(Result): _type = dict(wrong_key_usage=bool, chain_model=bool, is_de_vs=bool) _map = dict(notations=Notation) + class VerifyResult(Result): _map = dict(signatures=Signature) + class ImportStatus(Result): pass + class ImportResult(Result): _map = dict(imports=ImportStatus) + class GenkeyResult(Result): _type = dict(primary=bool, sub=bool) + class KeylistResult(Result): _type = dict(truncated=bool) + class VFSMountResult(Result): pass + class EngineInfo(Result): pass diff --git a/lang/python/src/util.py b/lang/python/src/util.py index e4fca4c..320a823 100644 --- a/lang/python/src/util.py +++ b/lang/python/src/util.py @@ -17,10 +17,12 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import sys +del absolute_import, print_function, unicode_literals + + def process_constants(prefix, scope): """Called by the constant modules to load up the constants from the C library starting with PREFIX. Matching constants will be inserted @@ -30,17 +32,19 @@ def process_constants(prefix, scope): """ from . import gpgme index = len(prefix) - constants = {identifier[index:]: getattr(gpgme, identifier) - for identifier in dir(gpgme) - if identifier.startswith(prefix)} + constants = { + identifier[index:]: getattr(gpgme, identifier) + for identifier in dir(gpgme) if identifier.startswith(prefix) + } scope.update(constants) return list(constants.keys()) + def percent_escape(s): - return ''.join( - '%{0:2x}'.format(ord(c)) - if c == '+' or c == '"' or c == '%' or ord(c) <= 0x20 else c - for c in s) + return ''.join('%{0:2x}'.format(ord(c)) + if c == '+' or c == '"' or c == '%' or ord(c) <= 0x20 else c + for c in s) + # Python2/3 compatibility if sys.version_info[0] == 3: diff --git a/lang/python/tests/Makefile.am b/lang/python/tests/Makefile.am index 3864f8b..d26d33d 100644 --- a/lang/python/tests/Makefile.am +++ b/lang/python/tests/Makefile.am @@ -21,7 +21,8 @@ GPG_AGENT = gpg-agent test_srcdir = $(top_srcdir)/tests/gpg -TESTS_ENVIRONMENT = GNUPGHOME=$(abs_builddir) \ +GNUPGHOME=$(abs_builddir) +TESTS_ENVIRONMENT = GNUPGHOME=$(GNUPGHOME) \ LC_ALL=C GPG_AGENT_INFO= \ top_srcdir=$(top_srcdir) \ srcdir=$(srcdir) \ @@ -118,3 +119,7 @@ gpg.conf: gpg-agent.conf: # This is required for gpg2, which does not support command fd. echo pinentry-program $(abs_top_srcdir)/tests/gpg/pinentry >$@ + echo disable-scdaemon >> $@ + + +# end-of-file diff --git a/lang/python/tests/Makefile.in b/lang/python/tests/Makefile.in index c7512dd..9efd7ee 100644 --- a/lang/python/tests/Makefile.in +++ b/lang/python/tests/Makefile.in @@ -113,7 +113,7 @@ am__aclocal_m4_deps = $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ $(ACLOCAL_M4) mkinstalldirs = $(SHELL) $(top_srcdir)/build-aux/mkinstalldirs -CONFIG_HEADER = $(top_builddir)/config.h +CONFIG_HEADER = $(top_builddir)/conf/config.h CONFIG_CLEAN_FILES = CONFIG_CLEAN_VPATH_FILES = AM_V_P = $(am__v_P_@AM_V@) @@ -334,7 +334,8 @@ top_srcdir = @top_srcdir@ GPG = gpg GPG_AGENT = gpg-agent test_srcdir = $(top_srcdir)/tests/gpg -TESTS_ENVIRONMENT = GNUPGHOME=$(abs_builddir) \ +GNUPGHOME = $(abs_builddir) +TESTS_ENVIRONMENT = GNUPGHOME=$(GNUPGHOME) \ LC_ALL=C GPG_AGENT_INFO= \ top_srcdir=$(top_srcdir) \ srcdir=$(srcdir) \ @@ -626,6 +627,9 @@ gpg.conf: gpg-agent.conf: # This is required for gpg2, which does not support command fd. echo pinentry-program $(abs_top_srcdir)/tests/gpg/pinentry >$@ + echo disable-scdaemon >> $@ + +# end-of-file # Tell versions [3.59,3.63) of GNU make to not export all variables. # Otherwise a system limit (for SysV at least) may be exceeded. diff --git a/lang/python/tests/final.py b/lang/python/tests/final.py index 65375cb..d0d52dc 100755 --- a/lang/python/tests/final.py +++ b/lang/python/tests/final.py @@ -18,12 +18,15 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import os import subprocess import support -_ = support # to appease pyflakes. +_ = support # to appease pyflakes. + +del absolute_import, print_function, unicode_literals -subprocess.check_call([os.path.join(os.getenv('top_srcdir'), - "tests", "start-stop-agent"), "--stop"]) +subprocess.check_call([ + os.path.join(os.getenv('top_srcdir'), "tests", "start-stop-agent"), + "--stop" +]) diff --git a/lang/python/tests/initial.py b/lang/python/tests/initial.py index 49e4f82..30a8de7 100755 --- a/lang/python/tests/initial.py +++ b/lang/python/tests/initial.py @@ -18,17 +18,20 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import os import subprocess import gpg import support +del absolute_import, print_function, unicode_literals + print("Using gpg module from {0!r}.".format(os.path.dirname(gpg.__file__))) -subprocess.check_call([os.path.join(os.getenv('top_srcdir'), - "tests", "start-stop-agent"), "--start"]) +subprocess.check_call([ + os.path.join(os.getenv('top_srcdir'), "tests", "start-stop-agent"), + "--start" +]) with gpg.Context() as c: alpha = c.get_key("A0FF4590BB6122EDEF6E3C542D727CC768697734", False) diff --git a/lang/python/tests/run-tests.py b/lang/python/tests/run-tests.py index 95df197..cec13b5 100644 --- a/lang/python/tests/run-tests.py +++ b/lang/python/tests/run-tests.py @@ -17,10 +17,8 @@ # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, see <http://www.gnu.org/licenses/>. -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals +from __future__ import absolute_import, division +from __future__ import print_function, unicode_literals import argparse import glob @@ -28,34 +26,50 @@ import os import subprocess import sys +del absolute_import, division, print_function, unicode_literals + + class SplitAndAccumulate(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): current = getattr(namespace, self.dest, list()) current.extend(values.split()) setattr(namespace, self.dest, current) + parser = argparse.ArgumentParser(description='Run tests.') -parser.add_argument('tests', metavar='TEST', type=str, nargs='+', - help='A test to run') -parser.add_argument('-v', '--verbose', action="store_true", default=False, - help='Be verbose.') -parser.add_argument('-q', '--quiet', action="store_true", default=False, - help='Be quiet.') -parser.add_argument('--interpreters', metavar='PYTHON', type=str, - default=[], action=SplitAndAccumulate, - help='Use these interpreters to run the tests, ' + - 'separated by spaces.') -parser.add_argument('--srcdir', type=str, - default=os.environ.get("srcdir", ""), - help='Location of the tests.') -parser.add_argument('--builddir', type=str, - default=os.environ.get("abs_builddir", ""), - help='Location of the tests.') -parser.add_argument('--python-libdir', type=str, - default=None, - help='Optional location of the in-tree module lib directory.') -parser.add_argument('--parallel', action="store_true", default=False, - help='Ignored. For compatibility with run-tests.scm.') +parser.add_argument( + 'tests', metavar='TEST', type=str, nargs='+', help='A test to run') +parser.add_argument( + '-v', '--verbose', action="store_true", default=False, help='Be verbose.') +parser.add_argument( + '-q', '--quiet', action="store_true", default=False, help='Be quiet.') +parser.add_argument( + '--interpreters', + metavar='PYTHON', + type=str, + default=[], + action=SplitAndAccumulate, + help='Use these interpreters to run the tests, ' + 'separated by spaces.') +parser.add_argument( + '--srcdir', + type=str, + default=os.environ.get("srcdir", ""), + help='Location of the tests.') +parser.add_argument( + '--builddir', + type=str, + default=os.environ.get("abs_builddir", ""), + help='Location of the tests.') +parser.add_argument( + '--python-libdir', + type=str, + default=None, + help='Optional location of the in-tree module lib directory.') +parser.add_argument( + '--parallel', + action="store_true", + default=False, + help='Ignored. For compatibility with run-tests.scm.') args = parser.parse_args() if not args.interpreters: @@ -64,26 +78,31 @@ if not args.interpreters: out = sys.stdout if args.verbose else None err = sys.stderr if args.verbose else None + def status_to_str(code): return {0: "PASS", 77: "SKIP", 99: "ERROR"}.get(code, "FAIL") + results = list() for interpreter in args.interpreters: - version = subprocess.check_output( - [interpreter, "-c", "import sys; print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))"]).strip().decode() + version = subprocess.check_output([ + interpreter, "-c", + "import sys; print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1]))" + ]).strip().decode() if args.python_libdir: python_libdir = args.python_libdir else: - pattern = os.path.join(args.builddir, "..", - "{0}-gpg".format(os.path.basename(interpreter)), - "lib*") + pattern = os.path.join(args.builddir, "..", "{0}-gpg".format( + os.path.basename(interpreter)), "lib*") libdirs = glob.glob(pattern) if len(libdirs) == 0: - sys.exit("Build directory matching {0!r} not found.".format(pattern)) + sys.exit( + "Build directory matching {0!r} not found.".format(pattern)) elif len(libdirs) > 1: - sys.exit("Multiple build directories matching {0!r} found: {1}".format( - pattern, libdirs)) + sys.exit( + "Multiple build directories matching {0!r} found: {1}".format( + pattern, libdirs)) python_libdir = libdirs[0] env = dict(os.environ) @@ -95,16 +114,22 @@ for interpreter in args.interpreters: for test in args.tests: status = subprocess.call( [interpreter, os.path.join(args.srcdir, test)], - env=env, stdout=out, stderr=err) + env=env, + stdout=out, + stderr=err) if not args.quiet: print("{0}: {1}".format(status_to_str(status), test)) results.append(status) + def count(status): return len(list(filter(lambda x: x == status, results))) + + def failed(): return len(list(filter(lambda x: x not in (0, 77, 99), results))) + if not args.quiet: print("{0} tests run, {1} succeeded, {2} failed, {3} skipped.".format( len(results), count(0), failed(), count(77))) diff --git a/lang/python/tests/support.py b/lang/python/tests/support.py index efccf31..e6b3d8b 100644 --- a/lang/python/tests/support.py +++ b/lang/python/tests/support.py @@ -16,7 +16,6 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import contextlib import shutil @@ -27,20 +26,28 @@ import tempfile import time import gpg +del absolute_import, print_function, unicode_literals + + def assert_gpg_version(version=(2, 1, 0)): with gpg.Context() as c: - clean_version = re.match(r'\d+\.\d+\.\d+', c.engine_info.version).group(0) + clean_version = re.match(r'\d+\.\d+\.\d+', + c.engine_info.version).group(0) if tuple(map(int, clean_version.split('.'))) < version: print("GnuPG too old: have {0}, need {1}.".format( c.engine_info.version, '.'.join(map(str, version)))) sys.exit(77) + def have_tofu_support(ctx, some_uid): - keys = list(ctx.keylist(some_uid, - mode=(gpg.constants.keylist.mode.LOCAL - |gpg.constants.keylist.mode.WITH_TOFU))) + keys = list( + ctx.keylist( + some_uid, + mode=(gpg.constants.keylist.mode.LOCAL | + gpg.constants.keylist.mode.WITH_TOFU))) return len(keys) > 0 + # Skip the Python tests for GnuPG < 2.1.12. Prior versions do not # understand the command line flags that we assume exist. C.f. issue # 3008. @@ -53,13 +60,18 @@ encrypt_only = "F52770D5C4DB41408D918C9F920572769B9FE19C" sign_only = "7CCA20CCDE5394CEE71C9F0BFED153F12F18F45D" no_such_key = "A" * 40 + def make_filename(name): return os.path.join(os.environ['top_srcdir'], 'tests', 'gpg', name) + def in_srcdir(name): return os.path.join(os.environ['srcdir'], name) + verbose = int(os.environ.get('verbose', 0)) > 1 + + def print_data(data): if verbose: try: @@ -75,10 +87,12 @@ def print_data(data): else: sys.stdout.write(data) + def mark_key_trusted(ctx, key): class Editor(object): def __init__(self): self.steps = ["trust", "save"] + def edit(self, status, args, out): if args == "keyedit.prompt": result = self.steps.pop(0) @@ -91,6 +105,7 @@ def mark_key_trusted(ctx, key): else: result = None return result + with gpg.Data() as sink: ctx.op_edit(key, Editor().edit, sink, sink) @@ -103,9 +118,11 @@ class TemporaryDirectory(object): def __enter__(self): self.path = tempfile.mkdtemp() return self.path + def __exit__(self, *args): shutil.rmtree(self.path, ignore_errors=True) + @contextlib.contextmanager def EphemeralContext(): with TemporaryDirectory() as tmp: @@ -124,7 +141,7 @@ def EphemeralContext(): ctx.assuan_transact(["KILLAGENT"]) except gpg.errors.GPGMEError as e: if e.getcode() == gpg.errors.ASS_CONNECT_FAILED: - pass # the agent was not running + pass # the agent was not running else: raise diff --git a/lang/python/tests/t-callbacks.py b/lang/python/tests/t-callbacks.py index 9a70cda..25a1c23 100755 --- a/lang/python/tests/t-callbacks.py +++ b/lang/python/tests/t-callbacks.py @@ -18,12 +18,13 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import os import gpg import support -_ = support # to appease pyflakes. +_ = support # to appease pyflakes. + +del absolute_import, print_function, unicode_literals c = gpg.Context() c.set_pinentry_mode(gpg.constants.PINENTRY_MODE_LOOPBACK) @@ -33,6 +34,7 @@ sink = gpg.Data() # Valid passphrases, both as string and bytes. for passphrase in ('foo', b'foo'): + def passphrase_cb(hint, desc, prev_bad, hook=None): assert hook == passphrase return hook @@ -40,10 +42,12 @@ for passphrase in ('foo', b'foo'): c.set_passphrase_cb(passphrase_cb, passphrase) c.op_encrypt([], 0, source, sink) + # Returning an invalid type. def passphrase_cb(hint, desc, prev_bad, hook=None): return 0 + c.set_passphrase_cb(passphrase_cb, None) try: c.op_encrypt([], 0, source, sink) @@ -55,9 +59,12 @@ else: # Raising an exception inside callback. myException = Exception() + + def passphrase_cb(hint, desc, prev_bad, hook=None): raise myException + c.set_passphrase_cb(passphrase_cb, None) try: c.op_encrypt([], 0, source, sink) @@ -66,10 +73,12 @@ except Exception as e: else: assert False, "Expected an error, got none" + # Wrong kind of callback function. def bad_passphrase_cb(): pass + c.set_passphrase_cb(bad_passphrase_cb, None) try: c.op_encrypt([], 0, source, sink) @@ -78,8 +87,6 @@ except Exception as e: else: assert False, "Expected an error, got none" - - # Test the progress callback. parms = """<GnupgKeyParms format="internal"> Key-Type: RSA @@ -88,26 +95,31 @@ Name-Real: Joe Tester Name-Comment: with stupid passphrase Name-Email: joe+gpg@example.org Passphrase: Crypt0R0cks -Expire-Date: 2020-12-31 +Expire-Date: 2099-12-31 </GnupgKeyParms> """ messages = [] + + def progress_cb(what, typ, current, total, hook=None): assert hook == messages messages.append( "PROGRESS UPDATE: what = {}, type = {}, current = {}, total = {}" .format(what, typ, current, total)) + c = gpg.Context() c.set_progress_cb(progress_cb, messages) c.op_genkey(parms, None, None) assert len(messages) > 0 + # Test exception handling. def progress_cb(what, typ, current, total, hook=None): raise myException + c = gpg.Context() c.set_progress_cb(progress_cb, None) try: @@ -117,7 +129,6 @@ except Exception as e: else: assert False, "Expected an error, got none" - # Test the edit callback. c = gpg.Context() c.set_pinentry_mode(gpg.constants.PINENTRY_MODE_LOOPBACK) @@ -127,11 +138,15 @@ alpha = c.get_key("A0FF4590BB6122EDEF6E3C542D727CC768697734", False) cookie = object() edit_cb_called = False + + def edit_cb(status, args, hook): global edit_cb_called edit_cb_called = True assert hook == cookie return "quit" if args == "keyedit.prompt" else None + + c.op_edit(alpha, edit_cb, cookie, sink) assert edit_cb_called @@ -141,8 +156,11 @@ c.set_pinentry_mode(gpg.constants.PINENTRY_MODE_LOOPBACK) c.set_passphrase_cb(lambda *args: "abc") sink = gpg.Data() + def edit_cb(status, args): raise myException + + try: c.op_edit(alpha, edit_cb, None, sink) except Exception as e: @@ -150,18 +168,19 @@ except Exception as e: else: assert False, "Expected an error, got none" - - # Test the status callback. source = gpg.Data("Hallo Leute\n") sink = gpg.Data() status_cb_called = False + + def status_cb(keyword, args, hook=None): global status_cb_called status_cb_called = True assert hook == cookie + c = gpg.Context() c.set_status_cb(status_cb, cookie) c.set_ctx_flag("full-status", "1") @@ -172,9 +191,11 @@ assert status_cb_called source = gpg.Data("Hallo Leute\n") sink = gpg.Data() + def status_cb(keyword, args): raise myException + c = gpg.Context() c.set_status_cb(status_cb, None) c.set_ctx_flag("full-status", "1") @@ -186,13 +207,16 @@ else: assert False, "Expected an error, got none" - # Test the data callbacks. def read_cb(amount, hook=None): assert hook == cookie return 0 + + def release_cb(hook=None): assert hook == cookie + + data = gpg.Data(cbs=(read_cb, None, None, release_cb, cookie)) try: data.read() @@ -201,8 +225,11 @@ except Exception as e: else: assert False, "Expected an error, got none" + def read_cb(amount): raise myException + + data = gpg.Data(cbs=(read_cb, None, None, lambda: None)) try: data.read() @@ -215,6 +242,8 @@ else: def write_cb(what, hook=None): assert hook == cookie return "wrong type" + + data = gpg.Data(cbs=(None, write_cb, None, release_cb, cookie)) try: data.write(b'stuff') @@ -223,8 +252,11 @@ except Exception as e: else: assert False, "Expected an error, got none" + def write_cb(what): raise myException + + data = gpg.Data(cbs=(None, write_cb, None, lambda: None)) try: data.write(b'stuff') @@ -237,6 +269,8 @@ else: def seek_cb(offset, whence, hook=None): assert hook == cookie return "wrong type" + + data = gpg.Data(cbs=(None, None, seek_cb, release_cb, cookie)) try: data.seek(0, os.SEEK_SET) @@ -245,8 +279,11 @@ except Exception as e: else: assert False, "Expected an error, got none" + def seek_cb(offset, whence): raise myException + + data = gpg.Data(cbs=(None, None, seek_cb, lambda: None)) try: data.seek(0, os.SEEK_SET) diff --git a/lang/python/tests/t-data.py b/lang/python/tests/t-data.py index 5cf074c..006c11f 100755 --- a/lang/python/tests/t-data.py +++ b/lang/python/tests/t-data.py @@ -18,14 +18,15 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import io import os import tempfile import gpg import support -_ = support # to appease pyflakes. +_ = support # to appease pyflakes. + +del absolute_import, print_function, unicode_literals data = gpg.Data('Hello world!') assert data.read() == b'Hello world!' @@ -94,7 +95,8 @@ with tempfile.NamedTemporaryFile() as tmp: # Open using name, offset, and length. data = gpg.Data(file=tmp.name, offset=23, length=42) - assert data.read() == binjunk[23:23+42] + assert data.read() == binjunk[23:23 + 42] + # Test callbacks. class DataObject(object): @@ -118,6 +120,7 @@ class DataObject(object): assert not self.released self.released = True + do = DataObject() cookie = object() data = gpg.Data(cbs=(do.read, do.write, do.seek, do.release, cookie)) diff --git a/lang/python/tests/t-decrypt-verify.py b/lang/python/tests/t-decrypt-verify.py index 03bbc4b..991d18c 100755 --- a/lang/python/tests/t-decrypt-verify.py +++ b/lang/python/tests/t-decrypt-verify.py @@ -18,11 +18,13 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import gpg import support +del absolute_import, print_function, unicode_literals + + def check_verify_result(result, summary, fpr, status): assert len(result.signatures) == 1, "Unexpected number of signatures" sig = result.signatures[0] @@ -32,7 +34,9 @@ def check_verify_result(result, summary, fpr, status): assert len(sig.notations) == 0 assert not sig.wrong_key_usage assert sig.validity == gpg.constants.validity.FULL - assert gpg.errors.GPGMEError(sig.validity_reason).getcode() == gpg.errors.NO_ERROR + assert gpg.errors.GPGMEError( + sig.validity_reason).getcode() == gpg.errors.NO_ERROR + c = gpg.Context() @@ -47,10 +51,9 @@ assert not result.unsupported_algorithm, \ support.print_data(sink) verify_result = c.op_verify_result() -check_verify_result(verify_result, - gpg.constants.sigsum.VALID | gpg.constants.sigsum.GREEN, - "A0FF4590BB6122EDEF6E3C542D727CC768697734", - gpg.errors.NO_ERROR) +check_verify_result( + verify_result, gpg.constants.sigsum.VALID | gpg.constants.sigsum.GREEN, + "A0FF4590BB6122EDEF6E3C542D727CC768697734", gpg.errors.NO_ERROR) # Idiomatic interface. with gpg.Context() as c: @@ -60,15 +63,14 @@ with gpg.Context() as c: c.decrypt(open(support.make_filename("cipher-2.asc")), verify=[alpha]) assert plaintext.find(b'Wenn Sie dies lesen k') >= 0, \ 'Plaintext not found' - check_verify_result(verify_result, - gpg.constants.sigsum.VALID | gpg.constants.sigsum.GREEN, - "A0FF4590BB6122EDEF6E3C542D727CC768697734", - gpg.errors.NO_ERROR) + check_verify_result( + verify_result, gpg.constants.sigsum.VALID | gpg.constants.sigsum.GREEN, + "A0FF4590BB6122EDEF6E3C542D727CC768697734", gpg.errors.NO_ERROR) try: - c.decrypt(open(support.make_filename("cipher-2.asc")), - verify=[alpha, bob]) - except gpg.errors.MissingSignatures as e: + c.decrypt( + open(support.make_filename("cipher-2.asc")), verify=[alpha, bob]) + except Exception as e: assert len(e.missing) == 1 assert e.missing[0] == bob else: diff --git a/lang/python/tests/t-decrypt.py b/lang/python/tests/t-decrypt.py index 05b6d8b..f2417c9 100755 --- a/lang/python/tests/t-decrypt.py +++ b/lang/python/tests/t-decrypt.py @@ -18,11 +18,12 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import gpg import support +del absolute_import, print_function, unicode_literals + c = gpg.Context() source = gpg.Data(file=support.make_filename("cipher-1.asc")) diff --git a/lang/python/tests/t-edit.py b/lang/python/tests/t-edit.py index b1075a9..cbc17d9 100755 --- a/lang/python/tests/t-edit.py +++ b/lang/python/tests/t-edit.py @@ -19,13 +19,15 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import sys import os import gpg import support -_ = support # to appease pyflakes. +_ = support # to appease pyflakes. + +del absolute_import, print_function, unicode_literals + class KeyEditor(object): def __init__(self): @@ -47,11 +49,12 @@ class KeyEditor(object): result = None if self.verbose: - sys.stderr.write("Code: {}, args: {!r}, Returning: {!r}\n" - .format(status, args, result)) + sys.stderr.write("Code: {}, args: {!r}, Returning: {!r}\n".format( + status, args, result)) return result + c = gpg.Context() c.set_pinentry_mode(gpg.constants.PINENTRY_MODE_LOOPBACK) c.set_passphrase_cb(lambda *args: "abc") @@ -59,13 +62,15 @@ c.set_armor(True) # The deprecated interface. editor = KeyEditor() -c.interact(c.get_key("A0FF4590BB6122EDEF6E3C542D727CC768697734", False), - editor.edit_fnc) +c.interact( + c.get_key("A0FF4590BB6122EDEF6E3C542D727CC768697734", False), + editor.edit_fnc) assert editor.done # The deprecated interface. sink = gpg.Data() editor = KeyEditor() -c.op_edit(c.get_key("A0FF4590BB6122EDEF6E3C542D727CC768697734", False), - editor.edit_fnc, sink, sink) +c.op_edit( + c.get_key("A0FF4590BB6122EDEF6E3C542D727CC768697734", False), + editor.edit_fnc, sink, sink) assert editor.done diff --git a/lang/python/tests/t-encrypt-large.py b/lang/python/tests/t-encrypt-large.py index 5646085..18576ac 100755 --- a/lang/python/tests/t-encrypt-large.py +++ b/lang/python/tests/t-encrypt-large.py @@ -18,13 +18,14 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import sys import random import gpg import support +del absolute_import, print_function, unicode_literals + if len(sys.argv) == 2: nbytes = int(sys.argv[1]) else: @@ -33,6 +34,8 @@ else: c = gpg.Context() ntoread = nbytes + + def read_cb(amount): global ntoread chunk = ntoread if ntoread < amount else amount @@ -41,12 +44,16 @@ def read_cb(amount): assert chunk >= 0 return bytes(bytearray(random.randrange(256) for i in range(chunk))) + nwritten = 0 + + def write_cb(data): global nwritten nwritten += len(data) return len(data) + source = gpg.Data(cbs=(read_cb, None, None, lambda: None)) sink = gpg.Data(cbs=(None, write_cb, None, lambda: None)) @@ -61,5 +68,5 @@ assert not result.invalid_recipients, \ assert ntoread == 0 if support.verbose: - sys.stderr.write( - "plaintext={} bytes, ciphertext={} bytes\n".format(nbytes, nwritten)) + sys.stderr.write("plaintext={} bytes, ciphertext={} bytes\n".format( + nbytes, nwritten)) diff --git a/lang/python/tests/t-encrypt-sign.py b/lang/python/tests/t-encrypt-sign.py index f04783f..84d1abb 100755 --- a/lang/python/tests/t-encrypt-sign.py +++ b/lang/python/tests/t-encrypt-sign.py @@ -18,15 +18,17 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import sys import gpg import support +del absolute_import, print_function, unicode_literals + c = gpg.Context() c.set_armor(True) + def check_result(r, typ): if r.invalid_signers: sys.exit("Invalid signer found: {}".format(r.invalid_signers.fpr)) @@ -42,7 +44,8 @@ def check_result(r, typ): sys.exit("Wrong pubkey algorithm reported: {}".format( signature.pubkey_algo)) - if signature.hash_algo not in (gpg.constants.md.SHA1, gpg.constants.md.RMD160): + if signature.hash_algo not in (gpg.constants.md.SHA1, + gpg.constants.md.RMD160): sys.exit("Wrong hash algorithm reported: {}".format( signature.hash_algo)) @@ -53,6 +56,7 @@ def check_result(r, typ): if signature.fpr != "A0FF4590BB6122EDEF6E3C542D727CC768697734": sys.exit("Wrong fingerprint reported: {}".format(signature.fpr)) + keys = [] keys.append(c.get_key("A0FF4590BB6122EDEF6E3C542D727CC768697734", False)) keys.append(c.get_key("D695676BDCEDCC2CDD6152BCFE180B1DA9E3B0B2", False)) @@ -61,7 +65,8 @@ for recipients in (keys, []): source = gpg.Data("Hallo Leute\n") sink = gpg.Data() - c.op_encrypt_sign(recipients, gpg.constants.ENCRYPT_ALWAYS_TRUST, source, sink) + c.op_encrypt_sign(recipients, gpg.constants.ENCRYPT_ALWAYS_TRUST, source, + sink) result = c.op_encrypt_result() assert not result.invalid_recipients, \ "Invalid recipient encountered: {}".format( @@ -72,13 +77,11 @@ for recipients in (keys, []): support.print_data(sink) - # Idiomatic interface. with gpg.Context(armor=True) as c: message = "Hallo Leute\n".encode() - ciphertext, _, sig_result = c.encrypt(message, - recipients=keys, - always_trust=True) + ciphertext, _, sig_result = c.encrypt( + message, recipients=keys, always_trust=True) assert len(ciphertext) > 0 assert ciphertext.find(b'BEGIN PGP MESSAGE') > 0, 'Marker not found' check_result(sig_result, gpg.constants.sig.mode.NORMAL) diff --git a/lang/python/tests/t-encrypt-sym.py b/lang/python/tests/t-encrypt-sym.py index 8299293..9b099fe 100755 --- a/lang/python/tests/t-encrypt-sym.py +++ b/lang/python/tests/t-encrypt-sym.py @@ -18,12 +18,13 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import os import gpg import support -_ = support # to appease pyflakes. +_ = support # to appease pyflakes. + +del absolute_import, print_function, unicode_literals for passphrase in ("abc", b"abc"): c = gpg.Context() @@ -34,6 +35,7 @@ for passphrase in ("abc", b"abc"): cipher = gpg.Data() passphrase_cb_called = 0 + def passphrase_cb(hint, desc, prev_bad, hook=None): global passphrase_cb_called passphrase_cb_called += 1 @@ -55,7 +57,7 @@ for passphrase in ("abc", b"abc"): c.op_decrypt(cipher, plain) # Seems like the passphrase is cached. - #assert passphrase_cb_called == 2, \ + # assert passphrase_cb_called == 2, \ # "Callback called {} times".format(passphrase_cb_called) support.print_data(plain) @@ -70,12 +72,12 @@ for passphrase in ("abc", b"abc"): # Check that the passphrase callback is not altered. def f(*args): assert False + c.set_passphrase_cb(f) message = "Hallo Leute\n".encode() - ciphertext, _, _ = c.encrypt(message, - passphrase=passphrase, - sign=False) + ciphertext, _, _ = c.encrypt( + message, passphrase=passphrase, sign=False) assert ciphertext.find(b'BEGIN PGP MESSAGE') > 0, 'Marker not found' plaintext, _, _ = c.decrypt(ciphertext, passphrase=passphrase) diff --git a/lang/python/tests/t-encrypt.py b/lang/python/tests/t-encrypt.py index 921502a..e702daa 100755 --- a/lang/python/tests/t-encrypt.py +++ b/lang/python/tests/t-encrypt.py @@ -18,11 +18,12 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import gpg import support +del absolute_import, print_function, unicode_literals + c = gpg.Context() c.set_armor(True) @@ -41,36 +42,37 @@ support.print_data(sink) # Idiomatic interface. with gpg.Context(armor=True) as c: - ciphertext, _, _ = c.encrypt("Hallo Leute\n".encode(), - recipients=keys, - sign=False, - always_trust=True) + ciphertext, _, _ = c.encrypt( + "Hallo Leute\n".encode(), + recipients=keys, + sign=False, + always_trust=True) assert len(ciphertext) > 0 assert ciphertext.find(b'BEGIN PGP MESSAGE') > 0, 'Marker not found' - c.encrypt("Hallo Leute\n".encode(), - recipients=[c.get_key(support.encrypt_only, False)], - sign=False, always_trust=True) + c.encrypt( + "Hallo Leute\n".encode(), + recipients=[c.get_key(support.encrypt_only, False)], + sign=False, + always_trust=True) try: - c.encrypt("Hallo Leute\n".encode(), - recipients=[c.get_key(support.sign_only, False)], - sign=False, always_trust=True) + c.encrypt( + "Hallo Leute\n".encode(), + recipients=[c.get_key(support.sign_only, False)], + sign=False, + always_trust=True) except gpg.errors.InvalidRecipients as e: assert len(e.recipients) == 1 assert support.sign_only.endswith(e.recipients[0].fpr) else: assert False, "Expected an InvalidRecipients error, got none" - - try: # People might be tempted to provide strings. # We should raise something useful. - ciphertext, _, _ = c.encrypt("Hallo Leute\n", - recipients=keys, - sign=False, - always_trust=True) + ciphertext, _, _ = c.encrypt( + "Hallo Leute\n", recipients=keys, sign=False, always_trust=True) except TypeError as e: # This test is a bit fragile, because the message # may very well change. So if the behaviour will change diff --git a/lang/python/tests/t-export.py b/lang/python/tests/t-export.py index b9d5204..6d771dd 100755 --- a/lang/python/tests/t-export.py +++ b/lang/python/tests/t-export.py @@ -18,11 +18,12 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import gpg import support +del absolute_import, print_function, unicode_literals + c = gpg.Context() c.set_armor(True) @@ -32,8 +33,8 @@ support.print_data(sink) # Again. Now using a key array. keys = [] -keys.append(c.get_key("0x68697734", False)) # Alpha -keys.append(c.get_key("0xA9E3B0B2", False)) # Bob +keys.append(c.get_key("0x68697734", False)) # Alpha +keys.append(c.get_key("0xA9E3B0B2", False)) # Bob sink = gpg.Data() c.op_export_keys(keys, 0, sink) support.print_data(sink) diff --git a/lang/python/tests/t-file-name.py b/lang/python/tests/t-file-name.py index 32fe84a..d9c226f 100755 --- a/lang/python/tests/t-file-name.py +++ b/lang/python/tests/t-file-name.py @@ -18,12 +18,13 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import os import gpg import support -_ = support # to appease pyflakes. +_ = support # to appease pyflakes. + +del absolute_import, print_function, unicode_literals testname = "abcde12345" diff --git a/lang/python/tests/t-idiomatic.py b/lang/python/tests/t-idiomatic.py index b7ae4eb..238bbf3 100755 --- a/lang/python/tests/t-idiomatic.py +++ b/lang/python/tests/t-idiomatic.py @@ -18,7 +18,6 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import sys import io @@ -26,7 +25,9 @@ import os import tempfile import gpg import support -_ = support # to appease pyflakes. +_ = support # to appease pyflakes. + +del absolute_import, print_function, unicode_literals # Both Context and Data can be used as context manager: with gpg.Context() as c, gpg.Data() as d: @@ -34,8 +35,9 @@ with gpg.Context() as c, gpg.Data() as d: d.write(b"Halloechen") leak_c = c leak_d = d -assert leak_c.wrapped == None -assert leak_d.wrapped == None +assert leak_c.wrapped is None +assert leak_d.wrapped is None + def sign_and_verify(source, signed, sink): with gpg.Context() as c: @@ -53,6 +55,7 @@ def sign_and_verify(source, signed, sink): sink.seek(0, os.SEEK_SET) assert sink.read() == b"Hallo Leute\n" + # Demonstrate automatic wrapping of file-like objects with 'fileno' # method. with tempfile.TemporaryFile() as source, \ @@ -73,7 +76,7 @@ if sys.version_info[0] == 3: bio.truncate(1) if len(bio.getvalue()) != 1: # This version of Python is affected, preallocate buffer. - preallocate = 128*b'\x00' + preallocate = 128 * b'\x00' else: preallocate = b'' diff --git a/lang/python/tests/t-import.py b/lang/python/tests/t-import.py index e2edf5a..82d3a4e 100755 --- a/lang/python/tests/t-import.py +++ b/lang/python/tests/t-import.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright (C) 2016 g10 Code GmbH +# Copyright (C) 2016 Tobias Mueller <muelli at cryptobitch.de> # # This file is part of GPGME. # @@ -18,45 +18,47 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import gpg import support +del absolute_import, print_function, unicode_literals + + def check_result(result, fpr, secret): assert result.considered == 1 or (secret and result.considered == 3) assert result.no_user_id == 0 - assert not ((secret and result.imported != 0) - or (not secret and (result.imported != 0 - and result.imported != 1))) + assert not ((secret and result.imported != 0) or + (not secret and + (result.imported != 0 and result.imported != 1))) assert result.imported_rsa == 0 - assert not ((secret and (result.unchanged != 0 and result.unchanged != 1)) - or (not secret and ((result.imported == 0 - and result.unchanged != 1) - or (result.imported == 1 - and result.unchanged != 0)))) + assert not ((secret and + (result.unchanged != 0 and result.unchanged != 1)) or + (not secret and + ((result.imported == 0 and result.unchanged != 1) or + (result.imported == 1 and result.unchanged != 0)))) assert result.new_user_ids == 0 assert result.new_sub_keys == 0 - assert not ((secret - and ((result.secret_imported == 0 - and result.new_signatures != 0) - or (result.secret_imported == 1 - and result.new_signatures > 1))) - or (not secret and result.new_signatures != 0)) + assert not ((secret and ( + (result.secret_imported == 0 and result.new_signatures != 0) or + (result.secret_imported == 1 and result.new_signatures > 1))) or + (not secret and result.new_signatures != 0)) assert result.new_revocations == 0 - assert not ((secret and result.secret_read != 1 and result.secret_read != 3) - or (not secret and result.secret_read != 0)) - assert not ((secret and result.secret_imported != 0 - and result.secret_imported != 1 - and result.secret_imported != 2) - or (not secret and result.secret_imported != 0)) - assert not ((secret - and ((result.secret_imported == 0 - and result.secret_unchanged != 1 - and result.secret_unchanged != 2) - or (result.secret_imported == 1 - and result.secret_unchanged != 0))) - or (not secret and result.secret_unchanged != 0)) + assert not ( + (secret and result.secret_read != 1 and result.secret_read != 3) or + (not secret and result.secret_read != 0)) + assert not ( + (secret and result.secret_imported != 0 and result. + secret_imported != 1 and result. + secret_imported != 2) or (not secret and result. + secret_imported != 0)) + assert not ((secret and + ((result.secret_imported == 0 and result. + secret_unchanged != 1 and result. + secret_unchanged != 2) or (result. + secret_imported == 1 and result. + secret_unchanged != 0))) or + (not secret and result.secret_unchanged != 0)) assert result.not_imported == 0 if secret: assert not (len(result.imports) in (0, 3)) @@ -67,12 +69,17 @@ def check_result(result, fpr, secret): assert len(result.imports) == 1 or fpr == result.imports[1].fpr assert result.imports[0].result == 0 + c = gpg.Context() -c.op_import(gpg.Data(file=support.make_filename("pubkey-1.asc"))) -result = c.op_import_result() +result = c.key_import(open(support.make_filename("pubkey-1.asc"), 'rb').read()) check_result(result, "ADAB7FCC1F4DE2616ECFA402AF82244F9CD9FD55", False) -c.op_import(gpg.Data(file=support.make_filename("seckey-1.asc"))) -result = c.op_import_result() +result = c.key_import(open(support.make_filename("seckey-1.asc"), 'rb').read()) check_result(result, "ADAB7FCC1F4DE2616ECFA402AF82244F9CD9FD55", True) + +try: + result = c.key_import(b"thisisnotakey") +except ValueError: + pass +assert result == "IMPORT_PROBLEM" diff --git a/lang/python/tests/t-keylist-from-data.py b/lang/python/tests/t-keylist-from-data.py index 6503eb7..4fd9ba0 100755 --- a/lang/python/tests/t-keylist-from-data.py +++ b/lang/python/tests/t-keylist-from-data.py @@ -18,87 +18,142 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import gpg import support +del absolute_import, print_function, unicode_literals + support.assert_gpg_version((2, 1, 14)) + # Check expration of keys. This test assumes three subkeys of which # 2 are expired; it is used with the "Whisky" test key. It has # already been checked that these 3 subkeys are available. def check_whisky(name, key): - sub1 = key.subkeys[2] - sub2 = key.subkeys[3] + sub1 = key.subkeys[2] + sub2 = key.subkeys[3] + + assert sub1.expired and sub2.expired, \ + "Subkey of `{}' not flagged as expired".format(name) + assert sub1.expires == 1129636886 and sub2.expires == 1129636939, \ + "Subkey of `{}' has wrong expiration date".format(name) - assert sub1.expired and sub2.expired, \ - "Subkey of `{}' not flagged as expired".format(name) - assert sub1.expires == 1129636886 and sub2.expires == 1129636939, \ - "Subkey of `{}' has wrong expiration date".format(name) keys = [ - [ "A0FF4590BB6122EDEF6E3C542D727CC768697734", "6AE6D7EE46A871F8", - [ [ "Alfa Test", "demo key", "alfa@example.net" ], - [ "Alpha Test", "demo key", "alpha@example.net" ], - [ "Alice", "demo key", "" ] ], 1 ], - [ "D695676BDCEDCC2CDD6152BCFE180B1DA9E3B0B2", "5381EA4EE29BA37F", - [ [ "Bob", "demo key", "" ], - [ "Bravo Test", "demo key", "bravo@example.net" ] ], 1 ], - [ "61EE841A2A27EB983B3B3C26413F4AF31AFDAB6C", "E71E72ACBC43DA60", - [ [ "Charlie Test", "demo key", "charlie@example.net" ] ], 1 ], - [ "6560C59C43D031C54D7C588EEBA9F240EB9DC9E6", "06F22880B0C45424", - [ [ "Delta Test", "demo key", "delta@example.net" ] ], 1 ], - [ "3531152DE293E26A07F504BC318C1FAEFAEF6D1B", "B5C79E1A7272144D", - [ [ "Echelon", "demo key", "" ], - [ "Echo Test", "demo key", "echo@example.net" ], - [ "Eve", "demo key", "" ] ], 1 ], - [ "56D33268F7FE693FBB594762D4BF57F37372E243", "0A32EE79EE45198E", - [ [ "Foxtrot Test", "demo key", "foxtrot@example.net" ] ], 1 ], - [ "C9C07DCC6621B9FB8D071B1D168410A48FC282E6", "247491CC9DCAD354", - [ [ "Golf Test", "demo key", "golf@example.net" ] ], 1 ], - [ "9E91CBB11E4D4135583EF90513DB965534C6E3F1", "76E26537D622AD0A", - [ [ "Hotel Test", "demo key", "hotel@example.net" ] ], 1 ], - [ "CD538D6CC9FB3D745ECDA5201FE8FC6F04259677", "C1C8EFDE61F76C73", - [ [ "India Test", "demo key", "india@example.net" ] ], 1 ], - [ "F8F1EDC73995AB739AD54B380C820C71D2699313", "BD0B108735F8F136", - [ [ "Juliet Test", "demo key", "juliet@example.net" ] ], 1 ], - [ "3FD11083779196C2ECDD9594AD1B0FAD43C2D0C7", "86CBB34A9AF64D02", - [ [ "Kilo Test", "demo key", "kilo@example.net" ] ], 1 ], - [ "1DDD28CEF714F5B03B8C246937CAB51FB79103F8", "0363B449FE56350C", - [ [ "Lima Test", "demo key", "lima@example.net" ] ], 1 ], - [ "2686AA191A278013992C72EBBE794852BE5CF886", "5F600A834F31EAE8", - [ [ "Mallory", "demo key", "" ], - [ "Mike Test", "demo key", "mike@example.net" ] ], 1 ], - [ "5AB9D6D7BAA1C95B3BAA3D9425B00FD430CEC684", "4C1D63308B70E472", - [ [ "November Test", "demo key", "november@example.net" ] ], 1 ], - [ "43929E89F8F79381678CAE515F6356BA6D9732AC", "FF0785712681619F", - [ [ "Oscar Test", "demo key", "oscar@example.net" ] ], 1 ], - [ "6FAA9C201E5E26DCBAEC39FD5D15E01D3FF13206", "2764E18263330D9C", - [ [ "Papa test", "demo key", "papa@example.net" ] ], 1 ], - [ "A7969DA1C3297AA96D49843F1C67EC133C661C84", "6CDCFC44A029ACF4", - [ [ "Quebec Test", "demo key", "quebec@example.net" ] ], 1 ], - [ "38FBE1E4BF6A5E1242C8F6A13BDBEDB1777FBED3", "9FAB805A11D102EA", - [ [ "Romeo Test", "demo key", "romeo@example.net" ] ], 1 ], - [ "045B2334ADD69FC221076841A5E67F7FA3AE3EA1", "93B88B0F0F1B50B4", - [ [ "Sierra Test", "demo key", "sierra@example.net" ] ], 1 ], - [ "ECAC774F4EEEB0620767044A58CB9A4C85A81F38", "97B60E01101C0402", - [ [ "Tango Test", "demo key", "tango@example.net" ] ], 1 ], - [ "0DBCAD3F08843B9557C6C4D4A94C0F75653244D6", "93079B915522BDB9", - [ [ "Uniform Test", "demo key", "uniform@example.net" ] ], 1 ], - [ "E8143C489C8D41124DC40D0B47AF4B6961F04784", "04071FB807287134", - [ [ "Victor Test", "demo key", "victor@example.org" ] ], 1 ], - [ "E8D6C90B683B0982BD557A99DEF0F7B8EC67DBDE", "D7FBB421FD6E27F6", - [ [ "Whisky Test", "demo key", "whisky@example.net" ] ], 3, - check_whisky ], - [ "04C1DF62EFA0EBB00519B06A8979A6C5567FB34A", "5CC6F87F41E408BE", - [ [ "XRay Test", "demo key", "xray@example.net" ] ], 1 ], - [ "ED9B316F78644A58D042655A9EEF34CD4B11B25F", "5ADFD255F7B080AD", - [ [ "Yankee Test", "demo key", "yankee@example.net" ] ], 1 ], - [ "23FD347A419429BACCD5E72D6BC4778054ACD246", "EF9DC276A172C881", - [ [ "Zulu Test", "demo key", "zulu@example.net" ] ], 1 ], + [ + "A0FF4590BB6122EDEF6E3C542D727CC768697734", "6AE6D7EE46A871F8", + [["Alfa Test", "demo key", "alfa@example.net"], + ["Alpha Test", "demo key", "alpha@example.net"], + ["Alice", "demo key", ""]], 1 + ], + [ + "D695676BDCEDCC2CDD6152BCFE180B1DA9E3B0B2", "5381EA4EE29BA37F", + [["Bob", "demo key", ""], + ["Bravo Test", "demo key", "bravo@example.net"]], 1 + ], + [ + "61EE841A2A27EB983B3B3C26413F4AF31AFDAB6C", "E71E72ACBC43DA60", + [["Charlie Test", "demo key", "charlie@example.net"]], 1 + ], + [ + "6560C59C43D031C54D7C588EEBA9F240EB9DC9E6", "06F22880B0C45424", + [["Delta Test", "demo key", "delta@example.net"]], 1 + ], + [ + "3531152DE293E26A07F504BC318C1FAEFAEF6D1B", "B5C79E1A7272144D", + [["Echelon", "demo key", + ""], ["Echo Test", "demo key", "echo@example.net"], + ["Eve", "demo key", ""]], 1 + ], + [ + "56D33268F7FE693FBB594762D4BF57F37372E243", "0A32EE79EE45198E", + [["Foxtrot Test", "demo key", "foxtrot@example.net"]], 1 + ], + [ + "C9C07DCC6621B9FB8D071B1D168410A48FC282E6", "247491CC9DCAD354", + [["Golf Test", "demo key", "golf@example.net"]], 1 + ], + [ + "9E91CBB11E4D4135583EF90513DB965534C6E3F1", "76E26537D622AD0A", + [["Hotel Test", "demo key", "hotel@example.net"]], 1 + ], + [ + "CD538D6CC9FB3D745ECDA5201FE8FC6F04259677", "C1C8EFDE61F76C73", + [["India Test", "demo key", "india@example.net"]], 1 + ], + [ + "F8F1EDC73995AB739AD54B380C820C71D2699313", "BD0B108735F8F136", + [["Juliet Test", "demo key", "juliet@example.net"]], 1 + ], + [ + "3FD11083779196C2ECDD9594AD1B0FAD43C2D0C7", "86CBB34A9AF64D02", + [["Kilo Test", "demo key", "kilo@example.net"]], 1 + ], + [ + "1DDD28CEF714F5B03B8C246937CAB51FB79103F8", "0363B449FE56350C", + [["Lima Test", "demo key", "lima@example.net"]], 1 + ], + [ + "2686AA191A278013992C72EBBE794852BE5CF886", "5F600A834F31EAE8", + [["Mallory", "demo key", ""], + ["Mike Test", "demo key", "mike@example.net"]], 1 + ], + [ + "5AB9D6D7BAA1C95B3BAA3D9425B00FD430CEC684", "4C1D63308B70E472", + [["November Test", "demo key", "november@example.net"]], 1 + ], + [ + "43929E89F8F79381678CAE515F6356BA6D9732AC", "FF0785712681619F", + [["Oscar Test", "demo key", "oscar@example.net"]], 1 + ], + [ + "6FAA9C201E5E26DCBAEC39FD5D15E01D3FF13206", "2764E18263330D9C", + [["Papa test", "demo key", "papa@example.net"]], 1 + ], + [ + "A7969DA1C3297AA96D49843F1C67EC133C661C84", "6CDCFC44A029ACF4", + [["Quebec Test", "demo key", "quebec@example.net"]], 1 + ], + [ + "38FBE1E4BF6A5E1242C8F6A13BDBEDB1777FBED3", "9FAB805A11D102EA", + [["Romeo Test", "demo key", "romeo@example.net"]], 1 + ], + [ + "045B2334ADD69FC221076841A5E67F7FA3AE3EA1", "93B88B0F0F1B50B4", + [["Sierra Test", "demo key", "sierra@example.net"]], 1 + ], + [ + "ECAC774F4EEEB0620767044A58CB9A4C85A81F38", "97B60E01101C0402", + [["Tango Test", "demo key", "tango@example.net"]], 1 + ], + [ + "0DBCAD3F08843B9557C6C4D4A94C0F75653244D6", "93079B915522BDB9", + [["Uniform Test", "demo key", "uniform@example.net"]], 1 + ], + [ + "E8143C489C8D41124DC40D0B47AF4B6961F04784", "04071FB807287134", + [["Victor Test", "demo key", "victor@example.org"]], 1 + ], + [ + "E8D6C90B683B0982BD557A99DEF0F7B8EC67DBDE", "D7FBB421FD6E27F6", + [["Whisky Test", "demo key", "whisky@example.net"]], 3, check_whisky + ], + [ + "04C1DF62EFA0EBB00519B06A8979A6C5567FB34A", "5CC6F87F41E408BE", + [["XRay Test", "demo key", "xray@example.net"]], 1 + ], + [ + "ED9B316F78644A58D042655A9EEF34CD4B11B25F", "5ADFD255F7B080AD", + [["Yankee Test", "demo key", "yankee@example.net"]], 1 + ], + [ + "23FD347A419429BACCD5E72D6BC4778054ACD246", "EF9DC276A172C881", + [["Zulu Test", "demo key", "zulu@example.net"]], 1 + ], ] + def check_global(key, uids, n_subkeys): assert not key.revoked, "Key unexpectedly revoked" assert not key.expired, "Key unexpectedly expired" @@ -145,10 +200,11 @@ def check_subkey(fpr, which, subkey): assert not subkey.secret, which + " key unexpectedly secret" assert not subkey.is_cardkey, "Public key marked as card key" assert not subkey.card_number, "Public key with card number set" - assert not subkey.pubkey_algo != (gpg.constants.pk.DSA if which == "Primary" - else gpg.constants.pk.ELG_E), \ - which + " key has unexpected public key algo: {}".\ - format(subkey.pubkey_algo) + assert not subkey.pubkey_algo != \ + (gpg.constants.pk.DSA if which == "Primary" + else gpg.constants.pk.ELG_E), \ + which + " key has unexpected public key algo: {}".format(subkey. + pubkey_algo) assert subkey.length == 1024, \ which + " key has unexpected length: {}".format(subkey.length) assert fpr.endswith(subkey.keyid), \ @@ -158,24 +214,26 @@ def check_subkey(fpr, which, subkey): assert not subkey.expires, \ which + " key unexpectedly expires: {}".format(subkey.expires) + def check_uid(which, ref, uid): assert not uid.revoked, which + " user ID unexpectedly revoked" assert not uid.invalid, which + " user ID unexpectedly invalid" assert uid.validity == gpg.constants.validity.UNKNOWN, \ - which + " user ID has unexpected validity: {}".format(uid.validity) + which + " user ID has unexpected validity: {}".format(uid.validity) assert not uid.signatures, which + " user ID unexpectedly signed" assert uid.name == ref[0], \ - "Unexpected name in {} user ID: {!r}".format(which.lower(), uid.name) + "Unexpected name in {} user ID: {!r}".format(which.lower(), uid.name) assert uid.comment == ref[1], \ - "Unexpected comment in {} user ID: {!r}".format(which.lower(), - uid.comment) + "Unexpected comment in {} user ID: {!r}".format(which.lower(), + uid.comment) assert uid.email == ref[2], \ - "Unexpected email in {} user ID: {!r}".format(which.lower(), uid.email) + "Unexpected email in {} user ID: {!r}".format(which.lower(), uid.email) + # Export all the data from our keyring... key_data = gpg.Data() with gpg.Context() as c: - c.op_export_keys([c.get_key(k[0]) for k in keys], 0, key_data) + c.op_export_keys([c.get_key(k[0]) for k in keys], 0, key_data) # ... rewind the tape... key_data.rewind() @@ -201,11 +259,11 @@ with support.EphemeralContext() as c: assert len(key.uids) == len(uids) check_uid("First", uids[0], key.uids[0]) if len(key.uids) > 1: - check_uid("Second", uids[1], key.uids[1]) + check_uid("Second", uids[1], key.uids[1]) if len(key.uids) > 2: - check_uid("Third", uids[2], key.uids[2]) + check_uid("Third", uids[2], key.uids[2]) if misc_check: - misc_check (uids[0][0], key) + misc_check(uids[0][0], key) assert len(list(c.keylist())) == 0, "Keys were imported" diff --git a/lang/python/tests/t-keylist.py b/lang/python/tests/t-keylist.py index 4505d3c..9cbada5 100755 --- a/lang/python/tests/t-keylist.py +++ b/lang/python/tests/t-keylist.py @@ -18,87 +18,142 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import gpg import support +del absolute_import, print_function, unicode_literals + c = gpg.Context() + # Check expration of keys. This test assumes three subkeys of which # 2 are expired; it is used with the "Whisky" test key. It has # already been checked that these 3 subkeys are available. def check_whisky(name, key): - sub1 = key.subkeys[2] - sub2 = key.subkeys[3] + sub1 = key.subkeys[2] + sub2 = key.subkeys[3] + + assert sub1.expired and sub2.expired, \ + "Subkey of `{}' not flagged as expired".format(name) + assert sub1.expires == 1129636886 and sub2.expires == 1129636939, \ + "Subkey of `{}' has wrong expiration date".format(name) - assert sub1.expired and sub2.expired, \ - "Subkey of `{}' not flagged as expired".format(name) - assert sub1.expires == 1129636886 and sub2.expires == 1129636939, \ - "Subkey of `{}' has wrong expiration date".format(name) keys = [ - [ "A0FF4590BB6122EDEF6E3C542D727CC768697734", "6AE6D7EE46A871F8", - [ [ "Alfa Test", "demo key", "alfa@example.net" ], - [ "Alpha Test", "demo key", "alpha@example.net" ], - [ "Alice", "demo key", "" ] ], 1 ], - [ "D695676BDCEDCC2CDD6152BCFE180B1DA9E3B0B2", "5381EA4EE29BA37F", - [ [ "Bob", "demo key", "" ], - [ "Bravo Test", "demo key", "bravo@example.net" ] ], 1 ], - [ "61EE841A2A27EB983B3B3C26413F4AF31AFDAB6C", "E71E72ACBC43DA60", - [ [ "Charlie Test", "demo key", "charlie@example.net" ] ], 1 ], - [ "6560C59C43D031C54D7C588EEBA9F240EB9DC9E6", "06F22880B0C45424", - [ [ "Delta Test", "demo key", "delta@example.net" ] ], 1 ], - [ "3531152DE293E26A07F504BC318C1FAEFAEF6D1B", "B5C79E1A7272144D", - [ [ "Echelon", "demo key", "" ], - [ "Echo Test", "demo key", "echo@example.net" ], - [ "Eve", "demo key", "" ] ], 1 ], - [ "56D33268F7FE693FBB594762D4BF57F37372E243", "0A32EE79EE45198E", - [ [ "Foxtrot Test", "demo key", "foxtrot@example.net" ] ], 1 ], - [ "C9C07DCC6621B9FB8D071B1D168410A48FC282E6", "247491CC9DCAD354", - [ [ "Golf Test", "demo key", "golf@example.net" ] ], 1 ], - [ "9E91CBB11E4D4135583EF90513DB965534C6E3F1", "76E26537D622AD0A", - [ [ "Hotel Test", "demo key", "hotel@example.net" ] ], 1 ], - [ "CD538D6CC9FB3D745ECDA5201FE8FC6F04259677", "C1C8EFDE61F76C73", - [ [ "India Test", "demo key", "india@example.net" ] ], 1 ], - [ "F8F1EDC73995AB739AD54B380C820C71D2699313", "BD0B108735F8F136", - [ [ "Juliet Test", "demo key", "juliet@example.net" ] ], 1 ], - [ "3FD11083779196C2ECDD9594AD1B0FAD43C2D0C7", "86CBB34A9AF64D02", - [ [ "Kilo Test", "demo key", "kilo@example.net" ] ], 1 ], - [ "1DDD28CEF714F5B03B8C246937CAB51FB79103F8", "0363B449FE56350C", - [ [ "Lima Test", "demo key", "lima@example.net" ] ], 1 ], - [ "2686AA191A278013992C72EBBE794852BE5CF886", "5F600A834F31EAE8", - [ [ "Mallory", "demo key", "" ], - [ "Mike Test", "demo key", "mike@example.net" ] ], 1 ], - [ "5AB9D6D7BAA1C95B3BAA3D9425B00FD430CEC684", "4C1D63308B70E472", - [ [ "November Test", "demo key", "november@example.net" ] ], 1 ], - [ "43929E89F8F79381678CAE515F6356BA6D9732AC", "FF0785712681619F", - [ [ "Oscar Test", "demo key", "oscar@example.net" ] ], 1 ], - [ "6FAA9C201E5E26DCBAEC39FD5D15E01D3FF13206", "2764E18263330D9C", - [ [ "Papa test", "demo key", "papa@example.net" ] ], 1 ], - [ "A7969DA1C3297AA96D49843F1C67EC133C661C84", "6CDCFC44A029ACF4", - [ [ "Quebec Test", "demo key", "quebec@example.net" ] ], 1 ], - [ "38FBE1E4BF6A5E1242C8F6A13BDBEDB1777FBED3", "9FAB805A11D102EA", - [ [ "Romeo Test", "demo key", "romeo@example.net" ] ], 1 ], - [ "045B2334ADD69FC221076841A5E67F7FA3AE3EA1", "93B88B0F0F1B50B4", - [ [ "Sierra Test", "demo key", "sierra@example.net" ] ], 1 ], - [ "ECAC774F4EEEB0620767044A58CB9A4C85A81F38", "97B60E01101C0402", - [ [ "Tango Test", "demo key", "tango@example.net" ] ], 1 ], - [ "0DBCAD3F08843B9557C6C4D4A94C0F75653244D6", "93079B915522BDB9", - [ [ "Uniform Test", "demo key", "uniform@example.net" ] ], 1 ], - [ "E8143C489C8D41124DC40D0B47AF4B6961F04784", "04071FB807287134", - [ [ "Victor Test", "demo key", "victor@example.org" ] ], 1 ], - [ "E8D6C90B683B0982BD557A99DEF0F7B8EC67DBDE", "D7FBB421FD6E27F6", - [ [ "Whisky Test", "demo key", "whisky@example.net" ] ], 3, - check_whisky ], - [ "04C1DF62EFA0EBB00519B06A8979A6C5567FB34A", "5CC6F87F41E408BE", - [ [ "XRay Test", "demo key", "xray@example.net" ] ], 1 ], - [ "ED9B316F78644A58D042655A9EEF34CD4B11B25F", "5ADFD255F7B080AD", - [ [ "Yankee Test", "demo key", "yankee@example.net" ] ], 1 ], - [ "23FD347A419429BACCD5E72D6BC4778054ACD246", "EF9DC276A172C881", - [ [ "Zulu Test", "demo key", "zulu@example.net" ] ], 1 ], + [ + "A0FF4590BB6122EDEF6E3C542D727CC768697734", "6AE6D7EE46A871F8", + [["Alfa Test", "demo key", + "alfa@example.net"], ["Alpha Test", "demo key", "alpha@example.net"], + ["Alice", "demo key", ""]], 1 + ], + [ + "D695676BDCEDCC2CDD6152BCFE180B1DA9E3B0B2", "5381EA4EE29BA37F", + [["Bob", "demo key", ""], + ["Bravo Test", "demo key", "bravo@example.net"]], 1 + ], + [ + "61EE841A2A27EB983B3B3C26413F4AF31AFDAB6C", "E71E72ACBC43DA60", + [["Charlie Test", "demo key", "charlie@example.net"]], 1 + ], + [ + "6560C59C43D031C54D7C588EEBA9F240EB9DC9E6", "06F22880B0C45424", + [["Delta Test", "demo key", "delta@example.net"]], 1 + ], + [ + "3531152DE293E26A07F504BC318C1FAEFAEF6D1B", "B5C79E1A7272144D", + [["Echelon", "demo key", + ""], ["Echo Test", "demo key", "echo@example.net"], + ["Eve", "demo key", ""]], 1 + ], + [ + "56D33268F7FE693FBB594762D4BF57F37372E243", "0A32EE79EE45198E", + [["Foxtrot Test", "demo key", "foxtrot@example.net"]], 1 + ], + [ + "C9C07DCC6621B9FB8D071B1D168410A48FC282E6", "247491CC9DCAD354", + [["Golf Test", "demo key", "golf@example.net"]], 1 + ], + [ + "9E91CBB11E4D4135583EF90513DB965534C6E3F1", "76E26537D622AD0A", + [["Hotel Test", "demo key", "hotel@example.net"]], 1 + ], + [ + "CD538D6CC9FB3D745ECDA5201FE8FC6F04259677", "C1C8EFDE61F76C73", + [["India Test", "demo key", "india@example.net"]], 1 + ], + [ + "F8F1EDC73995AB739AD54B380C820C71D2699313", "BD0B108735F8F136", + [["Juliet Test", "demo key", "juliet@example.net"]], 1 + ], + [ + "3FD11083779196C2ECDD9594AD1B0FAD43C2D0C7", "86CBB34A9AF64D02", + [["Kilo Test", "demo key", "kilo@example.net"]], 1 + ], + [ + "1DDD28CEF714F5B03B8C246937CAB51FB79103F8", "0363B449FE56350C", + [["Lima Test", "demo key", "lima@example.net"]], 1 + ], + [ + "2686AA191A278013992C72EBBE794852BE5CF886", "5F600A834F31EAE8", + [["Mallory", "demo key", ""], + ["Mike Test", "demo key", "mike@example.net"]], 1 + ], + [ + "5AB9D6D7BAA1C95B3BAA3D9425B00FD430CEC684", "4C1D63308B70E472", + [["November Test", "demo key", "november@example.net"]], 1 + ], + [ + "43929E89F8F79381678CAE515F6356BA6D9732AC", "FF0785712681619F", + [["Oscar Test", "demo key", "oscar@example.net"]], 1 + ], + [ + "6FAA9C201E5E26DCBAEC39FD5D15E01D3FF13206", "2764E18263330D9C", + [["Papa test", "demo key", "papa@example.net"]], 1 + ], + [ + "A7969DA1C3297AA96D49843F1C67EC133C661C84", "6CDCFC44A029ACF4", + [["Quebec Test", "demo key", "quebec@example.net"]], 1 + ], + [ + "38FBE1E4BF6A5E1242C8F6A13BDBEDB1777FBED3", "9FAB805A11D102EA", + [["Romeo Test", "demo key", "romeo@example.net"]], 1 + ], + [ + "045B2334ADD69FC221076841A5E67F7FA3AE3EA1", "93B88B0F0F1B50B4", + [["Sierra Test", "demo key", "sierra@example.net"]], 1 + ], + [ + "ECAC774F4EEEB0620767044A58CB9A4C85A81F38", "97B60E01101C0402", + [["Tango Test", "demo key", "tango@example.net"]], 1 + ], + [ + "0DBCAD3F08843B9557C6C4D4A94C0F75653244D6", "93079B915522BDB9", + [["Uniform Test", "demo key", "uniform@example.net"]], 1 + ], + [ + "E8143C489C8D41124DC40D0B47AF4B6961F04784", "04071FB807287134", + [["Victor Test", "demo key", "victor@example.org"]], 1 + ], + [ + "E8D6C90B683B0982BD557A99DEF0F7B8EC67DBDE", "D7FBB421FD6E27F6", + [["Whisky Test", "demo key", "whisky@example.net"]], 3, check_whisky + ], + [ + "04C1DF62EFA0EBB00519B06A8979A6C5567FB34A", "5CC6F87F41E408BE", + [["XRay Test", "demo key", "xray@example.net"]], 1 + ], + [ + "ED9B316F78644A58D042655A9EEF34CD4B11B25F", "5ADFD255F7B080AD", + [["Yankee Test", "demo key", "yankee@example.net"]], 1 + ], + [ + "23FD347A419429BACCD5E72D6BC4778054ACD246", "EF9DC276A172C881", + [["Zulu Test", "demo key", "zulu@example.net"]], 1 + ], ] + def check_global(key, uids, n_subkeys): assert not key.revoked, "Key unexpectedly revoked" assert not key.expired, "Key unexpectedly expired" @@ -117,12 +172,12 @@ def check_global(key, uids, n_subkeys): "Key unexpectedly carries chain ID: {}".format(key.chain_id) # Only key Alfa is trusted - assert key.uids[0].name == 'Alfa Test' \ - or key.owner_trust == gpg.constants.validity.UNKNOWN, \ - "Key has unexpected owner trust: {}".format(key.owner_trust) - assert key.uids[0].name != 'Alfa Test' \ - or key.owner_trust == gpg.constants.validity.ULTIMATE, \ - "Key has unexpected owner trust: {}".format(key.owner_trust) + assert (key.uids[0].name == 'Alfa Test' + or key.owner_trust == gpg.constants.validity.UNKNOWN), \ + "Key has unexpected owner trust: {}".format(key.owner_trust) + assert (key.uids[0].name != 'Alfa Test' + or key.owner_trust == gpg.constants.validity.ULTIMATE), \ + "Key has unexpected owner trust: {}".format(key.owner_trust) assert len(key.subkeys) - 1 == n_subkeys, \ "Key `{}' has unexpected number of subkeys".format(uids[0][0]) @@ -152,10 +207,11 @@ def check_subkey(fpr, which, subkey): assert not subkey.secret, which + " key unexpectedly secret" assert not subkey.is_cardkey, "Public key marked as card key" assert not subkey.card_number, "Public key with card number set" - assert not subkey.pubkey_algo != (gpg.constants.pk.DSA if which == "Primary" - else gpg.constants.pk.ELG_E), \ - which + " key has unexpected public key algo: {}".\ - format(subkey.pubkey_algo) + assert not subkey.pubkey_algo != \ + (gpg.constants.pk.DSA if which == "Primary" + else gpg.constants.pk.ELG_E), \ + which + " key has unexpected public key algo: {}".format(subkey. + pubkey_algo) assert subkey.length == 1024, \ which + " key has unexpected length: {}".format(subkey.length) assert fpr.endswith(subkey.keyid), \ @@ -165,6 +221,7 @@ def check_subkey(fpr, which, subkey): assert not subkey.expires, \ which + " key unexpectedly expires: {}".format(subkey.expires) + def check_uid(which, ref, uid): assert not uid.revoked, which + " user ID unexpectedly revoked" assert not uid.invalid, which + " user ID unexpectedly invalid" @@ -172,19 +229,20 @@ def check_uid(which, ref, uid): if uid.name.split()[0] not in {'Alfa', 'Alpha', 'Alice'} else gpg.constants.validity.ULTIMATE), \ - which + " user ID has unexpectedly validity: {}".format(uid.validity) + which + " user ID has unexpectedly validity: {}".format(uid.validity) assert not uid.signatures, which + " user ID unexpectedly signed" assert uid.name == ref[0], \ - "Unexpected name in {} user ID: {!r}".format(which.lower(), uid.name) + "Unexpected name in {} user ID: {!r}".format(which.lower(), uid.name) assert uid.comment == ref[1], \ - "Unexpected comment in {} user ID: {!r}".format(which.lower(), - uid.comment) + "Unexpected comment in {} user ID: {!r}".\ + format(which.lower(), uid.comment) assert uid.email == ref[2], \ - "Unexpected email in {} user ID: {!r}".format(which.lower(), uid.email) + "Unexpected email in {} user ID: {!r}".format(which.lower(), uid.email) + i = 0 c.op_keylist_start(None, False) -key = c.op_keylist_next () +key = c.op_keylist_next() while key: try: if len(keys[i]) == 4: @@ -204,20 +262,19 @@ while key: assert len(key.uids) == len(uids) check_uid("First", uids[0], key.uids[0]) if len(key.uids) > 1: - check_uid("Second", uids[1], key.uids[1]) + check_uid("Second", uids[1], key.uids[1]) if len(key.uids) > 2: - check_uid("Third", uids[2], key.uids[2]) + check_uid("Third", uids[2], key.uids[2]) if misc_check: - misc_check (uids[0][0], key) - key = c.op_keylist_next () + misc_check(uids[0][0], key) + key = c.op_keylist_next() i += 1 c.op_keylist_end() result = c.op_keylist_result() assert not result.truncated, "Key listing unexpectedly truncated" - # We test for a parameter-less keylist keyring_length = len(list(c.op_keylist_all())) assert keyring_length > 1,\ @@ -226,13 +283,12 @@ assert keyring_length > 1,\ # Then we do want to call with a pattern, only # i.e. without giving secret=0 alpha_keys = list(c.op_keylist_all(b"Alpha")) -assert len(alpha_keys) == 1, "Expected only one key for 'Alpha', got %r" % len(alpha_keys) - +assert len(alpha_keys) == 1, "Expected only one key for 'Alpha', got %r" % len( + alpha_keys) # Check negative result. assert len(list(c.keylist("no such key in sight"))) == 0 - for i, key in enumerate(c.keylist()): try: if len(keys[i]) == 4: @@ -252,31 +308,30 @@ for i, key in enumerate(c.keylist()): assert len(key.uids) == len(uids) check_uid("First", uids[0], key.uids[0]) if len(key.uids) > 1: - check_uid("Second", uids[1], key.uids[1]) + check_uid("Second", uids[1], key.uids[1]) if len(key.uids) > 2: - check_uid("Third", uids[2], key.uids[2]) + check_uid("Third", uids[2], key.uids[2]) if misc_check: - misc_check (uids[0][0], key) - + misc_check(uids[0][0], key) # check get_key() with gpg.Context() as c: - c.get_key(support.alpha) - c.get_key(support.alpha, secret=True) - - c.get_key(support.bob) - try: - c.get_key(support.bob, secret=True) - except KeyError: - pass - else: - assert False, "Expected KeyError" - - # Legacy error - try: - c.get_key(support.no_such_key) - except gpg.errors.GPGMEError: - pass - else: - assert False, "Expected GPGMEError" + c.get_key(support.alpha) + c.get_key(support.alpha, secret=True) + + c.get_key(support.bob) + try: + c.get_key(support.bob, secret=True) + except KeyError: + pass + else: + assert False, "Expected KeyError" + + # Legacy error + try: + c.get_key(support.no_such_key) + except gpg.errors.GPGMEError: + pass + else: + assert False, "Expected GPGMEError" diff --git a/lang/python/tests/t-protocol-assuan.py b/lang/python/tests/t-protocol-assuan.py index 8da5035..c337c3b 100755 --- a/lang/python/tests/t-protocol-assuan.py +++ b/lang/python/tests/t-protocol-assuan.py @@ -18,20 +18,21 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import gpg import support -_ = support # to appease pyflakes. +_ = support # to appease pyflakes. + +del absolute_import, print_function, unicode_literals with gpg.Context(protocol=gpg.constants.protocol.ASSUAN) as c: # Do nothing. err = c.assuan_transact('nop') - assert err == None + assert err is None err = c.assuan_transact(b'NOP') - assert err == None + assert err is None err = c.assuan_transact(['NOP']) - assert err == None + assert err is None err = c.assuan_transact('idontexist') assert err.getsource() == gpg.errors.SOURCE_GPGAGENT @@ -41,6 +42,7 @@ with gpg.Context(protocol=gpg.constants.protocol.ASSUAN) as c: c.assuan_transact(['GET_CONFIRMATION', 'Hello there']) data = [] + def data_cb(line): data.append(line) @@ -57,6 +59,7 @@ with gpg.Context(protocol=gpg.constants.protocol.ASSUAN) as c: # XXX HELP sends status lines if we could use ASSUAN_CONVEY_COMMENTS. status = [] + def status_cb(line, args): status.append((line, args)) diff --git a/lang/python/tests/t-quick-key-creation.py b/lang/python/tests/t-quick-key-creation.py index 8b7372e..b3303ff 100755 --- a/lang/python/tests/t-quick-key-creation.py +++ b/lang/python/tests/t-quick-key-creation.py @@ -18,7 +18,6 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import gpg import itertools @@ -27,6 +26,8 @@ import time import support support.assert_gpg_version((2, 1, 2)) +del absolute_import, print_function, unicode_literals + alpha = "Alpha <alpha@invalid.example.net>" with support.EphemeralContext() as ctx: @@ -51,14 +52,16 @@ with support.EphemeralContext() as ctx: res2 = ctx.create_key(alpha, force=True) assert res.fpr != res2.fpr - # From here on, we use one context, and create unique UIDs uid_counter = 0 + + def make_uid(): global uid_counter uid_counter += 1 return "user{0}@invalid.example.org".format(uid_counter) + with support.EphemeralContext() as ctx: # Check gpg.constants.create.NOEXPIRE... res = ctx.create_key(make_uid(), expires=False) @@ -77,10 +80,11 @@ with support.EphemeralContext() as ctx: "Primary keys expiration time is off" # Check capabilities - for sign, encrypt, certify, authenticate in itertools.product([False, True], - [False, True], - [False, True], - [False, True]): + for sign, encrypt, certify, authenticate \ + in itertools.product([False, True], + [False, True], + [False, True], + [False, True]): # Filter some out if not (sign or encrypt or certify or authenticate): # This triggers the default capabilities tested before. @@ -89,9 +93,13 @@ with support.EphemeralContext() as ctx: # The primary key always certifies. continue - res = ctx.create_key(make_uid(), algorithm="rsa", - sign=sign, encrypt=encrypt, certify=certify, - authenticate=authenticate) + res = ctx.create_key( + make_uid(), + algorithm="rsa", + sign=sign, + encrypt=encrypt, + certify=certify, + authenticate=authenticate) key = ctx.get_key(res.fpr, secret=True) assert key.fpr == res.fpr assert len(key.subkeys) == 1, \ @@ -125,13 +133,16 @@ with support.EphemeralContext() as ctx: recipient = make_uid() passphrase = "streng geheim" res = ctx.create_key(recipient, passphrase=passphrase) - ciphertext, _, _ = ctx.encrypt(b"hello there", recipients=[ctx.get_key(res.fpr)]) + ciphertext, _, _ = ctx.encrypt( + b"hello there", recipients=[ctx.get_key(res.fpr)]) cb_called = False + def cb(*args): global cb_called cb_called = True return passphrase + ctx.pinentry_mode = gpg.constants.PINENTRY_MODE_LOOPBACK ctx.set_passphrase_cb(cb) diff --git a/lang/python/tests/t-quick-key-manipulation.py b/lang/python/tests/t-quick-key-manipulation.py index 37e05b3..ade171e 100755 --- a/lang/python/tests/t-quick-key-manipulation.py +++ b/lang/python/tests/t-quick-key-manipulation.py @@ -18,7 +18,6 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import os import gpg @@ -27,6 +26,8 @@ import sys import support support.assert_gpg_version((2, 1, 14)) +del absolute_import, print_function, unicode_literals + alpha = "Alpha <alpha@invalid.example.net>" bravo = "Bravo <bravo@invalid.example.net>" @@ -111,9 +112,11 @@ with support.EphemeralContext() as ctx: ctx.key_tofu_policy(key, policy) - keys = list(ctx.keylist(key.uids[0].uid, - mode=(gpg.constants.keylist.mode.LOCAL - |gpg.constants.keylist.mode.WITH_TOFU))) + keys = list( + ctx.keylist( + key.uids[0].uid, + mode=(gpg.constants.keylist.mode.LOCAL | + gpg.constants.keylist.mode.WITH_TOFU))) assert len(keys) == 1 if policy == gpg.constants.tofu.policy.AUTO: diff --git a/lang/python/tests/t-quick-key-signing.py b/lang/python/tests/t-quick-key-signing.py index 3d648c5..6f9b8a7 100755 --- a/lang/python/tests/t-quick-key-signing.py +++ b/lang/python/tests/t-quick-key-signing.py @@ -18,7 +18,6 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import gpg import itertools @@ -27,8 +26,11 @@ import time import support support.assert_gpg_version((2, 1, 1)) +del absolute_import, print_function, unicode_literals + with support.EphemeralContext() as ctx: uid_counter = 0 + def make_uid(): global uid_counter uid_counter += 1 @@ -43,10 +45,16 @@ with support.EphemeralContext() as ctx: return key, uids def check_sigs(key, expected_sigs): - keys = list(ctx.keylist(key.fpr, mode=(gpg.constants.keylist.mode.LOCAL - |gpg.constants.keylist.mode.SIGS))) + keys = list( + ctx.keylist( + key.fpr, + mode=(gpg.constants.keylist.mode.LOCAL | + gpg.constants.keylist.mode.SIGS))) assert len(keys) == 1 - key_uids = {uid.uid: [s for s in uid.signatures] for uid in keys[0].uids} + key_uids = { + uid.uid: [s for s in uid.signatures] + for uid in keys[0].uids + } expected = list(expected_sigs) while key_uids and expected: @@ -76,9 +84,12 @@ with support.EphemeralContext() as ctx: assert s.exportable assert s.expires == 0 - check_sigs(key_b, itertools.product(uids_b, [key_b], [exportable_non_expiring])) + check_sigs(key_b, + itertools.product(uids_b, [key_b], [exportable_non_expiring])) ctx.key_sign(key_b) - check_sigs(key_b, itertools.product(uids_b, [key_b, key_a], [exportable_non_expiring])) + check_sigs( + key_b, + itertools.product(uids_b, [key_b, key_a], [exportable_non_expiring])) # Create a non-exportable signature, and explicitly name all uids. key_c, uids_c = make_key() @@ -89,11 +100,12 @@ with support.EphemeralContext() as ctx: assert s.expires == 0 ctx.key_sign(key_c, local=True, uids=uids_c) - check_sigs(key_c, - list(itertools.product(uids_c, [key_c], - [exportable_non_expiring])) - + list(itertools.product(uids_c, [key_b, key_a], - [non_exportable_non_expiring]))) + check_sigs( + key_c, + list(itertools.product(uids_c, [key_c], [exportable_non_expiring])) + + list( + itertools.product(uids_c, [key_b, key_a], + [non_exportable_non_expiring]))) # Create a non-exportable, expiring signature for a single uid. key_d, uids_d = make_key() @@ -106,16 +118,16 @@ with support.EphemeralContext() as ctx: assert abs(time.time() + expires_in - s.expires) < slack ctx.key_sign(key_d, local=True, expires_in=expires_in, uids=uids_d[0]) - check_sigs(key_d, - list(itertools.product(uids_d, [key_d], - [exportable_non_expiring])) - + list(itertools.product(uids_d[:1], [key_c], - [non_exportable_expiring]))) + check_sigs( + key_d, + list(itertools.product(uids_d, [key_d], [exportable_non_expiring])) + + list( + itertools.product(uids_d[:1], [key_c], [non_exportable_expiring]))) # Now sign the second in the same fashion, but use a singleton list. ctx.key_sign(key_d, local=True, expires_in=expires_in, uids=uids_d[1:2]) - check_sigs(key_d, - list(itertools.product(uids_d, [key_d], - [exportable_non_expiring])) - + list(itertools.product(uids_d[:2], [key_c], - [non_exportable_expiring]))) + check_sigs( + key_d, + list(itertools.product(uids_d, [key_d], [exportable_non_expiring])) + + list( + itertools.product(uids_d[:2], [key_c], [non_exportable_expiring]))) diff --git a/lang/python/tests/t-quick-subkey-creation.py b/lang/python/tests/t-quick-subkey-creation.py index ad4f35c..cdbb71b 100755 --- a/lang/python/tests/t-quick-subkey-creation.py +++ b/lang/python/tests/t-quick-subkey-creation.py @@ -18,7 +18,6 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import gpg import itertools @@ -26,6 +25,8 @@ import time import support +del absolute_import, print_function, unicode_literals + alpha = "Alpha <alpha@invalid.example.net>" bravo = "Bravo <bravo@invalid.example.net>" @@ -59,16 +60,17 @@ with support.EphemeralContext() as ctx: "subkeys expiration time is off" # Check capabilities - for sign, encrypt, authenticate in itertools.product([False, True], - [False, True], - [False, True]): + for sign, encrypt, authenticate \ + in itertools.product([False, True], + [False, True], + [False, True]): # Filter some out if not (sign or encrypt or authenticate): # This triggers the default capabilities tested before. continue - res = ctx.create_subkey(key, sign=sign, encrypt=encrypt, - authenticate=authenticate) + res = ctx.create_subkey( + key, sign=sign, encrypt=encrypt, authenticate=authenticate) subkey = get_subkey(res.fpr) assert sign == subkey.can_sign assert encrypt == subkey.can_encrypt @@ -92,18 +94,21 @@ with support.EphemeralContext() as ctx: # so that we have a key with just one encryption subkey. bravo_res = ctx.create_key(bravo, certify=True) bravo_key = ctx.get_key(bravo_res.fpr) - assert len(bravo_key.subkeys) == 1, "Expected one primary key and no subkeys" + assert len( + bravo_key.subkeys) == 1, "Expected one primary key and no subkeys" passphrase = "streng geheim" res = ctx.create_subkey(bravo_key, passphrase=passphrase) - ciphertext, _, _ = ctx.encrypt(b"hello there", - recipients=[ctx.get_key(bravo_res.fpr)]) + ciphertext, _, _ = ctx.encrypt( + b"hello there", recipients=[ctx.get_key(bravo_res.fpr)]) cb_called = False + def cb(*args): global cb_called cb_called = True return passphrase + ctx.pinentry_mode = gpg.constants.PINENTRY_MODE_LOOPBACK ctx.set_passphrase_cb(cb) diff --git a/lang/python/tests/t-sig-notation.py b/lang/python/tests/t-sig-notation.py index bc8da2e..5960f44 100755 --- a/lang/python/tests/t-sig-notation.py +++ b/lang/python/tests/t-sig-notation.py @@ -18,29 +18,30 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import os import gpg import support -_ = support # to appease pyflakes. +_ = support # to appease pyflakes. + +del absolute_import, print_function, unicode_literals expected_notations = { - "laughing@me": ("Just Squeeze Me", gpg.constants.sig.notation.HUMAN_READABLE), - "preferred-email-encoding@pgp.com": ("pgpmime", - gpg.constants.sig.notation.HUMAN_READABLE - | gpg.constants.sig.notation.CRITICAL), + "laughing@me": ("Just Squeeze Me", + gpg.constants.sig.notation.HUMAN_READABLE), + "preferred-email-encoding@pgp.com": + ("pgpmime", gpg.constants.sig.notation.HUMAN_READABLE | + gpg.constants.sig.notation.CRITICAL), None: ("http://www.gnu.org/policy/", 0), } # GnuPG prior to 2.1.13 did not report the critical flag correctly. with gpg.Context() as c: version = c.engine_info.version - have_correct_sig_data = not (version.startswith("1.") - or version.startswith("2.0.") - or version == "2.1.1" - or (version.startswith("2.1.1") - and version[5] < '3')) + have_correct_sig_data = not ( + version.startswith("1.") or version.startswith("2.0.") or + (version.startswith("2.1.") and int(version[4:]) < 13)) + def check_result(result): assert len(result.signatures) == 1, "Unexpected number of signatures" @@ -48,8 +49,8 @@ def check_result(result): assert len(sig.notations) == len(expected_notations) for r in sig.notations: - assert not 'name_len' in dir(r) - assert not 'value_len' in dir(r) + assert 'name_len' not in dir(r) + assert 'value_len' not in dir(r) assert r.name in expected_notations value, flags = expected_notations.pop(r.name) @@ -63,6 +64,7 @@ def check_result(result): assert len(expected_notations) == 0 + source = gpg.Data("Hallo Leute\n") signed = gpg.Data() diff --git a/lang/python/tests/t-sign.py b/lang/python/tests/t-sign.py index d375729..3ad05e8 100755 --- a/lang/python/tests/t-sign.py +++ b/lang/python/tests/t-sign.py @@ -18,15 +18,18 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import os import gpg import support +del absolute_import, print_function, unicode_literals + + def fail(msg): raise RuntimeError(msg) + def check_result(r, typ): if r.invalid_signers: fail("Invalid signer found: {}".format(r.invalid_signers.fpr)) @@ -43,16 +46,15 @@ def check_result(r, typ): signature.pubkey_algo)) if signature.hash_algo != gpg.constants.md.SHA1: - fail("Wrong hash algorithm reported: {}".format( - signature.hash_algo)) + fail("Wrong hash algorithm reported: {}".format(signature.hash_algo)) if signature.sig_class != 1: - fail("Wrong signature class reported: {}".format( - signature.sig_class)) + fail("Wrong signature class reported: {}".format(signature.sig_class)) if signature.fpr != "A0FF4590BB6122EDEF6E3C542D727CC768697734": fail("Wrong fingerprint reported: {}".format(signature.fpr)) + c = gpg.Context() c.set_textmode(True) c.set_armor(True) diff --git a/lang/python/tests/t-signers.py b/lang/python/tests/t-signers.py index 5864ee5..119ab77 100755 --- a/lang/python/tests/t-signers.py +++ b/lang/python/tests/t-signers.py @@ -18,14 +18,17 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import gpg import support +del absolute_import, print_function, unicode_literals + + def fail(msg): raise RuntimeError(msg) + def check_result(r, typ): if r.invalid_signers: fail("Invalid signer found: {}".format(r.invalid_signers.fpr)) @@ -53,6 +56,7 @@ def check_result(r, typ): "23FD347A419429BACCD5E72D6BC4778054ACD246"): fail("Wrong fingerprint reported: {}".format(signature.fpr)) + c = gpg.Context() c.set_textmode(True) c.set_armor(True) diff --git a/lang/python/tests/t-trustlist.py b/lang/python/tests/t-trustlist.py index 89524bb..ffa0b96 100755 --- a/lang/python/tests/t-trustlist.py +++ b/lang/python/tests/t-trustlist.py @@ -18,18 +18,21 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import gpg import support -_ = support # to appease pyflakes. +_ = support # to appease pyflakes. + +del absolute_import, print_function, unicode_literals c = gpg.Context() + def dump_item(item): - print("l={} k={} t={} o={} v={} u={}".format( - item.level, item.keyid, item.type, item.owner_trust, - item.validity, item.name)) + print("l={} k={} t={} o={} v={} u={}".format(item.level, item.keyid, + item.type, item.owner_trust, + item.validity, item.name)) + c.op_trustlist_start("alice", 0) while True: diff --git a/lang/python/tests/t-verify.py b/lang/python/tests/t-verify.py index 320dae6..70a6c1c 100755 --- a/lang/python/tests/t-verify.py +++ b/lang/python/tests/t-verify.py @@ -18,16 +18,17 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import sys import os import gpg import support -_ = support # to appease pyflakes. +_ = support # to appease pyflakes. + +del absolute_import, print_function, unicode_literals test_text1 = b"Just GNU it!\n" -test_text1f= b"Just GNU it?\n" +test_text1f = b"Just GNU it?\n" test_sig1 = b"""-----BEGIN PGP SIGNATURE----- iN0EABECAJ0FAjoS+i9FFIAAAAAAAwA5YmFyw7bDpMO8w58gZGFzIHdhcmVuIFVt @@ -60,6 +61,7 @@ UqVooWlGXHwNw/xg/fVzt9VNbtjtJ/fhUqYo0/LyCGEA -----END PGP MESSAGE----- """ + def check_result(result, summary, validity, fpr, status, notation): assert len(result.signatures) == 1, "Unexpected number of signatures" sig = result.signatures[0] @@ -76,14 +78,16 @@ def check_result(result, summary, validity, fpr, status, notation): if sys.version_info[0] < 3 else b"\xc3\xb6\xc3\xa4\xc3\xbc\xc3\x9f".decode() + " das waren Umlaute und jetzt ein prozent%-Zeichen"), - "foobar.1": "this is a notation data with 2 lines", - None: "http://www.gu.org/policy/", + "foobar.1": + "this is a notation data with 2 lines", + None: + "http://www.gu.org/policy/", } assert len(sig.notations) == len(expected_notations) for r in sig.notations: - assert not 'name_len' in dir(r) - assert not 'value_len' in dir(r) + assert 'name_len' not in dir(r) + assert 'value_len' not in dir(r) assert r.name in expected_notations assert r.value == expected_notations[r.name], \ "Expected {!r}, got {!r}".format(expected_notations[r.name], @@ -96,7 +100,9 @@ def check_result(result, summary, validity, fpr, status, notation): assert sig.validity == validity, \ "Unexpected signature validity: {}, want: {}".format( sig.validity, validity) - assert gpg.errors.GPGMEError(sig.validity_reason).getcode() == gpg.errors.NO_ERROR + assert gpg.errors.GPGMEError( + sig.validity_reason).getcode() == gpg.errors.NO_ERROR + c = gpg.Context() c.set_armor(True) @@ -108,9 +114,8 @@ c.op_verify(sig, text, None) result = c.op_verify_result() check_result(result, gpg.constants.sigsum.VALID | gpg.constants.sigsum.GREEN, gpg.constants.validity.FULL, - "A0FF4590BB6122EDEF6E3C542D727CC768697734", - gpg.errors.NO_ERROR, True) - + "A0FF4590BB6122EDEF6E3C542D727CC768697734", gpg.errors.NO_ERROR, + True) # Checking a manipulated message. text = gpg.Data(test_text1f) @@ -127,8 +132,8 @@ c.op_verify(sig, None, text) result = c.op_verify_result() check_result(result, gpg.constants.sigsum.VALID | gpg.constants.sigsum.GREEN, gpg.constants.validity.FULL, - "A0FF4590BB6122EDEF6E3C542D727CC768697734", - gpg.errors.NO_ERROR, False) + "A0FF4590BB6122EDEF6E3C542D727CC768697734", gpg.errors.NO_ERROR, + False) # Checking an invalid message. text = gpg.Data() @@ -141,33 +146,32 @@ except Exception as e: else: assert False, "Expected an error but got none." - # Idiomatic interface. with gpg.Context(armor=True) as c: # Checking a valid message. _, result = c.verify(test_text1, test_sig1) - check_result(result, gpg.constants.sigsum.VALID | gpg.constants.sigsum.GREEN, - gpg.constants.validity.FULL, - "A0FF4590BB6122EDEF6E3C542D727CC768697734", - gpg.errors.NO_ERROR, True) + check_result( + result, gpg.constants.sigsum.VALID | gpg.constants.sigsum.GREEN, + gpg.constants.validity.FULL, + "A0FF4590BB6122EDEF6E3C542D727CC768697734", gpg.errors.NO_ERROR, True) # Checking a manipulated message. try: c.verify(test_text1f, test_sig1) except gpg.errors.BadSignatures as e: check_result(e.result, gpg.constants.sigsum.RED, - gpg.constants.validity.UNKNOWN, - "2D727CC768697734", gpg.errors.BAD_SIGNATURE, False) + gpg.constants.validity.UNKNOWN, "2D727CC768697734", + gpg.errors.BAD_SIGNATURE, False) else: assert False, "Expected an error but got none." # Checking a normal signature. sig = gpg.Data(test_sig2) data, result = c.verify(test_sig2) - check_result(result, gpg.constants.sigsum.VALID | gpg.constants.sigsum.GREEN, - gpg.constants.validity.FULL, - "A0FF4590BB6122EDEF6E3C542D727CC768697734", - gpg.errors.NO_ERROR, False) + check_result( + result, gpg.constants.sigsum.VALID | gpg.constants.sigsum.GREEN, + gpg.constants.validity.FULL, + "A0FF4590BB6122EDEF6E3C542D727CC768697734", gpg.errors.NO_ERROR, False) assert data == test_text1 # Checking an invalid message. diff --git a/lang/python/tests/t-wait.py b/lang/python/tests/t-wait.py index 3101301..907f450 100755 --- a/lang/python/tests/t-wait.py +++ b/lang/python/tests/t-wait.py @@ -18,12 +18,13 @@ # License along with this program; if not, see <http://www.gnu.org/licenses/>. from __future__ import absolute_import, print_function, unicode_literals -del absolute_import, print_function, unicode_literals import time import gpg import support -_ = support # to appease pyflakes. +_ = support # to appease pyflakes. + +del absolute_import, print_function, unicode_literals c = gpg.Context() c.set_armor(True) diff --git a/lang/python/tests/t-wrapper.py b/lang/python/tests/t-wrapper.py index 08a320d..020e71e 100755 --- a/lang/python/tests/t-wrapper.py +++ b/lang/python/tests/t-wrapper.py @@ -19,9 +19,9 @@ import gpg import support -_ = support # to appease pyflakes. +_ = support # to appease pyflakes. d0 = gpg.Data() -d0.seek # trigger on-demand-wrapping +d0.seek # trigger on-demand-wrapping assert d0.seek == d0.seek, "Generated wrapper functions are not cached" assert hasattr(gpg.Data, 'seek'), "Generated wrapper functions are not shared" diff --git a/lang/python/version.py.in b/lang/python/version.py.in index 1a1baf0..ad76eda 100644 --- a/lang/python/version.py.in +++ b/lang/python/version.py.in @@ -1,4 +1,6 @@ -# Copyright (C) 2016 g10 Code GmbH +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2018 g10 Code GmbH # Copyright (C) 2015 Ben McGinnes <ben@adversary.org> # Copyright (C) 2004 Igor Belyi <belyi@users.sourceforge.net> # @@ -17,10 +19,11 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from __future__ import absolute_import, print_function -del absolute_import, print_function from . import gpgme +del absolute_import, print_function + productname = 'gpg' versionstr = "@VERSION@" gpgme_versionstr = gpgme.GPGME_VERSION @@ -32,8 +35,8 @@ minor = versionlist[1] patch = versionlist[2] copyright = """\ -Copyright (C) 2016 g10 Code GmbH -Copyright (C) 2015 Ben McGinnes +Copyright (C) 2016-2018 g10 Code GmbH +Copyright (C) 2015 Benjamin D. McGinnes Copyright (C) 2014-2015 Martin Albrecht Copyright (C) 2004-2008 Igor Belyi Copyright (C) 2002 John Goerzen""" @@ -44,8 +47,8 @@ author_email = "gnupg-devel@gnupg.org" description = "Python support for GPGME GnuPG cryptography library" homepage = "https://gnupg.org" -license = """Copyright (C) 2016 g10 Code GmbH -Copyright (C) 2015 Ben McGinnes <ben@adversary.org> +license = """Copyright (C) 2016-2018 g10 Code GmbH +Copyright (C) 2015 Benjamin D. McGinnes <ben@adversary.org> Copyright (C) 2014, 2015 Martin Albrecht <martinralbrecht@googlemail.com> Copyright (C) 2004, 2008 Igor Belyi <belyi@users.sourceforge.net> Copyright (C) 2002 John Goerzen <jgoerzen@complete.org> diff --git a/lang/qt/Makefile.am b/lang/qt/Makefile.am index ab85960..a1b83e8 100644 --- a/lang/qt/Makefile.am +++ b/lang/qt/Makefile.am @@ -19,6 +19,12 @@ # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA -SUBDIRS = src tests doc +if RUN_GPG_TESTS +tests = tests +else +tests = +endif + +SUBDIRS = src ${tests} doc EXTRA_DIST = README diff --git a/lang/qt/Makefile.in b/lang/qt/Makefile.in index c17e359..e979cfe 100644 --- a/lang/qt/Makefile.in +++ b/lang/qt/Makefile.in @@ -116,7 +116,7 @@ am__aclocal_m4_deps = $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ $(ACLOCAL_M4) mkinstalldirs = $(SHELL) $(top_srcdir)/build-aux/mkinstalldirs -CONFIG_HEADER = $(top_builddir)/config.h +CONFIG_HEADER = $(top_builddir)/conf/config.h CONFIG_CLEAN_FILES = CONFIG_CLEAN_VPATH_FILES = AM_V_P = $(am__v_P_@AM_V@) @@ -173,7 +173,7 @@ am__define_uniq_tagged_files = \ done | $(am__uniquify_input)` ETAGS = etags CTAGS = ctags -DIST_SUBDIRS = $(SUBDIRS) +DIST_SUBDIRS = src tests doc DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) am__relativize = \ dir0=`pwd`; \ @@ -394,7 +394,9 @@ target_alias = @target_alias@ top_build_prefix = @top_build_prefix@ top_builddir = @top_builddir@ top_srcdir = @top_srcdir@ -SUBDIRS = src tests doc +@RUN_GPG_TESTS_FALSE@tests = +@RUN_GPG_TESTS_TRUE@tests = tests +SUBDIRS = src ${tests} doc EXTRA_DIST = README all: all-recursive diff --git a/lang/qt/doc/Makefile.in b/lang/qt/doc/Makefile.in index 3b2b494..2dcd41b 100644 --- a/lang/qt/doc/Makefile.in +++ b/lang/qt/doc/Makefile.in @@ -114,7 +114,7 @@ am__aclocal_m4_deps = $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ $(ACLOCAL_M4) mkinstalldirs = $(SHELL) $(top_srcdir)/build-aux/mkinstalldirs -CONFIG_HEADER = $(top_builddir)/config.h +CONFIG_HEADER = $(top_builddir)/conf/config.h CONFIG_CLEAN_FILES = Doxyfile CONFIG_CLEAN_VPATH_FILES = AM_V_P = $(am__v_P_@AM_V@) diff --git a/lang/qt/src/Makefile.in b/lang/qt/src/Makefile.in index c35e288..9290424 100644 --- a/lang/qt/src/Makefile.in +++ b/lang/qt/src/Makefile.in @@ -102,7 +102,7 @@ am__aclocal_m4_deps = $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ $(ACLOCAL_M4) mkinstalldirs = $(SHELL) $(top_srcdir)/build-aux/mkinstalldirs -CONFIG_HEADER = $(top_builddir)/config.h +CONFIG_HEADER = $(top_builddir)/conf/config.h CONFIG_CLEAN_FILES = QGpgmeConfig-w32.cmake.in QGpgmeConfig.cmake.in \ QGpgmeConfigVersion.cmake qgpgme_version.h CONFIG_CLEAN_VPATH_FILES = @@ -181,7 +181,7 @@ AM_V_at = $(am__v_at_@AM_V@) am__v_at_ = $(am__v_at_@AM_DEFAULT_V@) am__v_at_0 = @ am__v_at_1 = -DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir) +DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)/conf depcomp = $(SHELL) $(top_srcdir)/build-aux/depcomp am__depfiles_maybe = depfiles am__mv = mv -f diff --git a/lang/qt/src/qgpgmenewcryptoconfig.cpp b/lang/qt/src/qgpgmenewcryptoconfig.cpp index ba028a9..070ab69 100644 --- a/lang/qt/src/qgpgmenewcryptoconfig.cpp +++ b/lang/qt/src/qgpgmenewcryptoconfig.cpp @@ -42,6 +42,7 @@ #include "gpgme_backend_debug.h" #include <QFile> +#include <QDir> #include "global.h" #include "error.h" @@ -521,8 +522,7 @@ QUrl QGpgMENewCryptoConfigEntry::urlValue() const Q_ASSERT(type == FilenameType || type == LdapServerType); Q_ASSERT(!isList()); if (type == FilenameType) { - QUrl url; - url.setPath(QFile::decodeName(m_option.currentValue().stringValue())); + QUrl url = QUrl::fromLocalFile(m_option.currentValue().stringValue()); return url; } return parseURL(type, stringValue()); @@ -635,7 +635,7 @@ void QGpgMENewCryptoConfigEntry::setURLValue(const QUrl &url) if (str.isEmpty() && !isOptional()) { m_option.resetToDefaultValue(); } else if (type == FilenameType) { - m_option.setNewValue(m_option.createStringArgument(QFile::encodeName(str).constData())); + m_option.setNewValue(m_option.createStringArgument(QDir::toNativeSeparators(url.toLocalFile()).toUtf8().constData())); } else { m_option.setNewValue(m_option.createStringArgument(str.toUtf8().constData())); } diff --git a/lang/qt/src/threadedjobmixin.cpp b/lang/qt/src/threadedjobmixin.cpp index 74755c5..cd7c494 100644 --- a/lang/qt/src/threadedjobmixin.cpp +++ b/lang/qt/src/threadedjobmixin.cpp @@ -53,7 +53,68 @@ using namespace QGpgME; using namespace GpgME; -static const unsigned int GetAuditLogFlags = Context::AuditLogWithHelp | Context::HtmlAuditLog; +#ifdef Q_OS_WIN +#include <windows.h> + +static QString fromEncoding (unsigned int src_encoding, const char *data) +{ + int n = MultiByteToWideChar(src_encoding, 0, data, -1, NULL, 0); + if (n < 0) { + return QString(); + } + + wchar_t *result = (wchar_t *) malloc ((n+1) * sizeof *result); + + n = MultiByteToWideChar(src_encoding, 0, data, -1, result, n); + if (n < 0) { + free(result); + return QString(); + } + const auto ret = QString::fromWCharArray(result, n); + free(result); + return ret; +} +#endif + +static QString stringFromGpgOutput(const QByteArray &ba) +{ +#ifdef Q_OS_WIN + /* Qt on Windows uses GetACP while GnuPG prefers + * GetConsoleOutputCP. + * + * As we are not a console application GetConsoleOutputCP + * usually returns 0. + * From experience the closest thing that let's us guess + * what GetConsoleOutputCP returns for a console application + * it appears to be the OEMCP. + */ + unsigned int cpno = GetConsoleOutputCP (); + if (!cpno) { + cpno = GetOEMCP(); + } + if (!cpno) { + cpno = GetACP(); + } + if (!cpno) { + return QString(); + } + + return fromEncoding(cpno, ba.constData()); +#else + return QString::fromLocal8Bit(ba); +#endif +} + +static QString markupDiagnostics(const QString &data) +{ + // First ensure that we don't have html in the diag. + QString ret = QStringLiteral("<pre>%1</pre>").arg(data.toHtmlEscaped()); + + return ret; +} + +static const unsigned int CMSAuditLogFlags = Context::AuditLogWithHelp | Context::HtmlAuditLog; +static const unsigned int OpenPGPAuditLogFlags = Context::DiagnosticAuditLog; QString _detail::audit_log_as_html(Context *ctx, GpgME::Error &err) { @@ -61,11 +122,24 @@ QString _detail::audit_log_as_html(Context *ctx, GpgME::Error &err) QGpgME::QByteArrayDataProvider dp; Data data(&dp); assert(!data.isNull()); - if ((err = ctx->lastError()) || (err = ctx->getAuditLog(data, GetAuditLogFlags))) { - return QString::fromLocal8Bit(err.asString()); + + if (ctx->protocol() == OpenPGP) { + if ((err = ctx->getAuditLog(data, OpenPGPAuditLogFlags))) { + return QString::fromLocal8Bit(err.asString()); + } + const QByteArray ba = dp.data(); + return markupDiagnostics(stringFromGpgOutput(ba)); } - const QByteArray ba = dp.data(); - return QString::fromUtf8(ba.data(), ba.size()); + + if (ctx->protocol() == CMS) { + if ((err = ctx->lastError()) || (err = ctx->getAuditLog(data, CMSAuditLogFlags))) { + return QString::fromLocal8Bit(err.asString()); + } + const QByteArray ba = dp.data(); + return QString::fromUtf8(ba.data(), ba.size()); + } + + return QStringLiteral("Unsupported protocol for Audit Log"); } static QList<QByteArray> from_sl(const QStringList &sl) diff --git a/lang/qt/tests/Makefile.am b/lang/qt/tests/Makefile.am index 104672e..08b1c4c 100644 --- a/lang/qt/tests/Makefile.am +++ b/lang/qt/tests/Makefile.am @@ -21,7 +21,8 @@ GPG = gpg -TESTS_ENVIRONMENT = GNUPGHOME=$(abs_builddir) +GNUPGHOME=$(abs_builddir) +TESTS_ENVIRONMENT = GNUPGHOME=$(GNUPGHOME) EXTRA_DIST = initial.test @@ -67,7 +68,7 @@ noinst_PROGRAMS = t-keylist t-keylocate t-ownertrust t-tofuinfo t-encrypt \ CLEANFILES = secring.gpg pubring.gpg pubring.kbx trustdb.gpg dirmngr.conf \ gpg-agent.conf pubring.kbx~ S.gpg-agent gpg.conf pubring.gpg~ \ random_seed S.gpg-agent .gpg-v21-migrated pubring-stamp $(moc_files) \ - gpg.conf tofu.db + gpg.conf tofu.db reader_0.status reader_1.status clean-local: -$(TESTS_ENVIRONMENT) $(top_srcdir)/tests/start-stop-agent --stop diff --git a/lang/qt/tests/Makefile.in b/lang/qt/tests/Makefile.in index 075747f..1546836 100644 --- a/lang/qt/tests/Makefile.in +++ b/lang/qt/tests/Makefile.in @@ -123,7 +123,7 @@ am__aclocal_m4_deps = $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ $(ACLOCAL_M4) mkinstalldirs = $(SHELL) $(top_srcdir)/build-aux/mkinstalldirs -CONFIG_HEADER = $(top_builddir)/config.h +CONFIG_HEADER = $(top_builddir)/conf/config.h CONFIG_CLEAN_FILES = CONFIG_CLEAN_VPATH_FILES = PROGRAMS = $(noinst_PROGRAMS) @@ -197,7 +197,7 @@ AM_V_at = $(am__v_at_@AM_V@) am__v_at_ = $(am__v_at_@AM_DEFAULT_V@) am__v_at_0 = @ am__v_at_1 = -DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir) +DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)/conf depcomp = $(SHELL) $(top_srcdir)/build-aux/depcomp am__depfiles_maybe = depfiles am__mv = mv -f @@ -490,7 +490,8 @@ top_build_prefix = @top_build_prefix@ top_builddir = @top_builddir@ top_srcdir = @top_srcdir@ GPG = gpg -TESTS_ENVIRONMENT = GNUPGHOME=$(abs_builddir) +GNUPGHOME = $(abs_builddir) +TESTS_ENVIRONMENT = GNUPGHOME=$(GNUPGHOME) EXTRA_DIST = initial.test moc_files = t-keylist.moc t-keylocate.moc t-ownertrust.moc t-tofuinfo.moc \ t-encrypt.moc t-support.hmoc t-wkspublish.moc t-verify.moc \ @@ -523,7 +524,7 @@ BUILT_SOURCES = $(moc_files) pubring-stamp CLEANFILES = secring.gpg pubring.gpg pubring.kbx trustdb.gpg dirmngr.conf \ gpg-agent.conf pubring.kbx~ S.gpg-agent gpg.conf pubring.gpg~ \ random_seed S.gpg-agent .gpg-v21-migrated pubring-stamp $(moc_files) \ - gpg.conf tofu.db + gpg.conf tofu.db reader_0.status reader_1.status all: $(BUILT_SOURCES) $(MAKE) $(AM_MAKEFLAGS) all-am diff --git a/lang/qt/tests/t-various.cpp b/lang/qt/tests/t-various.cpp index 7545628..76e6806 100644 --- a/lang/qt/tests/t-various.cpp +++ b/lang/qt/tests/t-various.cpp @@ -98,6 +98,25 @@ private Q_SLOTS: QVERIFY(key.primaryFingerprint() == QStringLiteral("7A0904B6950DA998020A1AD4BE41C0C3A5FF1F3C")); } + void testDataRewind() + { + if (GpgME::engineInfo(GpgME::GpgEngine).engineVersion() < "2.1.14") { + return; + } + QGpgME::QByteArrayDataProvider dp(aKey); + Data data(&dp); + char buf[20]; + data.read(buf, 20); + + auto keys = data.toKeys(); + QVERIFY(keys.size() == 0); + + data.rewind(); + + keys = data.toKeys(); + QVERIFY(keys.size() == 1); + } + void testQuickUid() { if (GpgME::engineInfo(GpgME::GpgEngine).engineVersion() < "2.1.13") { |