diff options
author | Michael Raitza <spacefrogg-devel@meterriblecrew.net> | 2020-07-28 21:08:39 +0200 |
---|---|---|
committer | John Wiegley <johnw@newartisans.com> | 2020-10-30 10:08:00 -0700 |
commit | f8e8cc160048cc7032254f1db7548c68571c3409 (patch) | |
tree | f9196d0624e1aa88924f4c0d33a767686026eba4 | |
parent | eaf095ec3b0017f824cdbfe4e9e4bc42331d421a (diff) | |
download | fork-ledger-f8e8cc160048cc7032254f1db7548c68571c3409.tar.gz fork-ledger-f8e8cc160048cc7032254f1db7548c68571c3409.tar.bz2 fork-ledger-f8e8cc160048cc7032254f1db7548c68571c3409.zip |
Implement cryptographic access to files using GPGME
-rw-r--r-- | .travis.yml | 8 | ||||
-rw-r--r-- | CMakeLists.txt | 14 | ||||
-rw-r--r-- | default.nix | 5 | ||||
-rw-r--r-- | src/CMakeLists.txt | 10 | ||||
-rw-r--r-- | src/context.h | 8 | ||||
-rw-r--r-- | src/error.cc | 17 | ||||
-rw-r--r-- | src/gpgme.cc | 220 | ||||
-rw-r--r-- | src/gpgme.h | 126 | ||||
-rw-r--r-- | src/system.hh.in | 1 |
9 files changed, 401 insertions, 8 deletions
diff --git a/.travis.yml b/.travis.yml index e56fcf30..e78076fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,7 +30,7 @@ addons: project: name: "ledger/ledger" description: "Build submitted via Travis CI" - build_command_prepend: "cmake . -DUSE_PYTHON=ON -DBUILD_DEBUG=ON -DCLANG_GCOV=ON -DPython_FIND_VERSION_MAJOR=${PY_MAJOR}" + build_command_prepend: "cmake . -DUSE_PYTHON=ON -DBUILD_DEBUG=ON -DUSE_GPGME=ON -DCLANG_GCOV=ON -DPython_FIND_VERSION_MAJOR=${PY_MAJOR}" build_command: "make" branch_pattern: coverity apt: @@ -47,6 +47,9 @@ addons: - libboost-iostreams-dev - libboost-filesystem-dev - libboost-serialization-dev + - libgpgmepp-dev + - libgpg-error-dev + - libgpgme-dev homebrew: update: true packages: @@ -55,13 +58,14 @@ addons: - boost-python3 - gmp - mpfr + - gpgme before_script: # On macOS boost-python packaging is broken - if [ "$TRAVIS_OS_NAME" = osx ]; then EXTRA_CMAKE_ARGS="-DBoost_NO_BOOST_CMAKE=ON"; fi # Ensure cmake locates python 3.8. Brew changed boost-python3 to use 3.8 but it isn't in the path by default - if [ "$TRAVIS_OS_NAME" = osx ]; then export PATH="/usr/local/opt/python@3.8/bin:$PATH"; fi - - cmake . -DUSE_PYTHON=ON -DPython_FIND_VERSION_MAJOR=${PY_MAJOR} -DBUILD_DEBUG=ON $EXTRA_CMAKE_ARGS + - cmake . -DUSE_PYTHON=ON -DPython_FIND_VERSION_MAJOR=${PY_MAJOR} -DUSE_GPGME=ON -DBUILD_DEBUG=ON $EXTRA_CMAKE_ARGS - make VERBOSE=1 script: diff --git a/CMakeLists.txt b/CMakeLists.txt index e06c31d9..15cb7ef9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,7 @@ option(DISABLE_ASSERTS "Build without any internal consistency checks" OFF) option(BUILD_DEBUG "Build support for runtime debugging" OFF) option(PRECOMPILE_SYSTEM_HH "Precompile system.hh" ON) +option(USE_GPGME "Build with support for encrypted journals" OFF) option(BUILD_LIBRARY "Build and install Ledger as a library" ON) option(BUILD_DOCS "Build and install documentation" OFF) option(BUILD_WEB_DOCS "Build version of documentation suitable for viewing online" OFF) @@ -86,6 +87,16 @@ find_package(Boost 1.49.0 include_directories(SYSTEM ${Boost_INCLUDE_DIRS}) link_directories(${Boost_LIBRARY_DIRS}) +# Crypto +if (USE_GPGME) + find_package(Gpgmepp REQUIRED) + set(HAVE_GPGME 1) + include_directories(SYSTEM ${Gpgmepp_INCLUDE_DIRS}) + link_directories(${Gpgmepp_LIBRARY_DIRS}) +else() + set(HAVE_GPGME 0) +endif() + ######################################################################## include(CheckIncludeFiles) @@ -256,6 +267,9 @@ macro(add_ledger_library_dependencies _target) if (HAVE_GETTEXT) target_link_libraries(${_target} ${INTL_LIB}) endif() + if (HAVE_GPGME) + target_link_libraries(${_target} Gpgmepp) + endif() if (HAVE_BOOST_PYTHON) if(CMAKE_SYSTEM_NAME STREQUAL Darwin) # Don't link directly to a Python framework on macOS, to avoid segfaults diff --git a/default.nix b/default.nix index c19f356e..ef26b4b4 100644 --- a/default.nix +++ b/default.nix @@ -24,11 +24,12 @@ pkgs.stdenv.mkDerivation { src = ./.; - buildInputs = with pkgs; [ cmake boost gmp mpfr libedit python texinfo gnused ]; + nativeBuildInputs = with pkgs; [ cmake ]; + buildInputs = with pkgs; [ boost gmp mpfr libedit python texinfo gnused gpgme ]; enableParallelBuilding = true; - cmakeFlags = [ "-DCMAKE_INSTALL_LIBDIR=lib" ]; + cmakeFlags = [ "-DCMAKE_INSTALL_LIBDIR=lib" "-DUSE_GPGME=1" ]; buildPhase = "make -j$NIX_BUILD_CORES"; checkPhase = "ctest -j$NIX_BUILD_CORES"; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b3d13a29..30d97cb9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -53,6 +53,11 @@ set(LEDGER_SOURCES utils.cc wcwidth.cc) +if (HAVE_GPGME) + list(APPEND LEDGER_SOURCES + gpgme.cc) +endif() + if (HAVE_BOOST_PYTHON) list(APPEND LEDGER_SOURCES py_account.cc @@ -94,6 +99,7 @@ set(LEDGER_INCLUDES format.h generate.h global.h + gpgme.h history.h item.h iterators.h @@ -284,6 +290,10 @@ if (BUILD_LIBRARY) add_executable(ledger main.cc global.cc) target_link_libraries(ledger libledger) + if (HAVE_GPGME) + target_link_libraries(ledger Gpgmepp) + endif() + if (HAVE_BOOST_PYTHON) target_link_libraries(ledger ${Python_LIBRARIES}) endif() diff --git a/src/context.h b/src/context.h index ca7af060..bf97feb5 100644 --- a/src/context.h +++ b/src/context.h @@ -45,6 +45,10 @@ #include "utils.h" #include "times.h" +#if HAVE_GPGME +#include "gpgme.h" +#endif + namespace ledger { class journal_t; @@ -119,7 +123,11 @@ inline parse_context_t open_for_reading(const path& pathname, _f("Cannot read journal file %1%") % filename); path parent(filename.parent_path()); +#if HAVE_GPGME + shared_ptr<std::istream> stream(decrypted_stream_t::open_stream(filename)); +#else shared_ptr<std::istream> stream(new ifstream(filename)); +#endif parse_context_t context(stream, parent); context.pathname = filename; return context; diff --git a/src/error.cc b/src/error.cc index 837d7499..ab7fab5b 100644 --- a/src/error.cc +++ b/src/error.cc @@ -33,6 +33,10 @@ #include "utils.h" +#if HAVE_GPGME +#include "gpgme.h" +#endif + namespace ledger { std::ostringstream _ctxt_buffer; @@ -92,12 +96,16 @@ string source_context(const path& file, std::ostringstream out; - ifstream in(file); - in.seekg(pos, std::ios::beg); +#if HAVE_GPGME + std::istream* in(decrypted_stream_t::open_stream(file)); +#else + std::istream* in(new ifstream(file)); +#endif + in->seekg(pos, std::ios::beg); scoped_array<char> buf(new char[static_cast<std::size_t>(len) + 1]); - in.read(buf.get(), static_cast<std::streamsize>(len)); - assert(in.gcount() == static_cast<std::streamsize>(len)); + in->read(buf.get(), static_cast<std::streamsize>(len)); + assert(in->gcount() == static_cast<std::streamsize>(len)); buf[static_cast<std::ptrdiff_t>(len)] = '\0'; bool first = true; @@ -111,6 +119,7 @@ string source_context(const path& file, out << prefix << p; } + delete(in); return out.str(); } diff --git a/src/gpgme.cc b/src/gpgme.cc new file mode 100644 index 00000000..4f949c8c --- /dev/null +++ b/src/gpgme.cc @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2020, Michael Raitza. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <system.hh> + +#include "gpgme.h" +#include "utils.h" + +#include <iostream> +#include <sstream> +#include <cerrno> + +#include <gpgme++/context.h> +#include <gpgme++/decryptionresult.h> + +namespace ledger { + +using GpgME::Context; +using GpgME::Data; +using GpgME::Protocol; +using namespace std; +using std::shared_ptr; +using std::unique_ptr; + +constexpr static unsigned int _bufsize = 4096; + +data_streambuffer_t::data_streambuffer_t(Data& _data) : + data(_data), + bufsize(_bufsize), + cbuf(unique_ptr<char[]>(new char[_bufsize])) {} + +streambuf::int_type data_streambuffer_t::underflow() { + if (this->gptr() == this->egptr()) { + + auto buf = cbuf.get(); + auto size = data.read(buf, bufsize); + if (size < 0) + throw_(runtime_error, _f("Reading decrypted data: GPGME: %1%") % strerror(errno)); + else if (size == 0) + return char_traits<char>::eof(); + + this->setg(buf, buf, buf + size); + } + return this->gptr() == this->egptr() + ? char_traits<char>::eof() + : char_traits<char>::to_int_type(*this->gptr()); +} + +streambuf::pos_type data_streambuffer_t::seekpos(streambuf::pos_type sp, ios_base::openmode which = ios_base::in) { + return this->seekoff(static_cast<streambuf::off_type>(sp), ios::beg, which); +} + +streambuf::pos_type data_streambuffer_t::seekoff(streambuf::off_type off, + ios_base::seekdir dir, + ios_base::openmode which = ios_base::in) { + streamoff pos = -1; + if (dir == ios::beg) + pos = static_cast<streambuf::pos_type>(data.seek(static_cast<streamoff>(off), SEEK_SET)); + else if (dir == ios::end) + pos = static_cast<streambuf::pos_type>(data.seek(static_cast<streamoff>(off), SEEK_END)); + else if (dir == ios::cur) + pos = static_cast<streambuf::pos_type>(data.seek(static_cast<streamoff>(off), SEEK_CUR)); + + if (pos == -1) + throw runtime_error("Unable to seek to position"); + + auto buf = cbuf.get(); + // Trigger underflow on next read + this->setg(buf, buf+1, buf+1); + return pos; +} + +FILE * decrypted_stream_t::open_file(const path& filename) { + FILE * f = fopen(filename.c_str(), "rb"); + if (!f) + throw_(runtime_error, _f("Could not open file: %1%") % strerror(errno)); + return f; +} + +shared_ptr<Data> decrypted_stream_t::setup_cipher_buffer(FILE * f) { + auto enc_d = make_shared<Data>(f); + if (!enc_d) + throw runtime_error("Unable to create cipher text buffer"); + + if (enc_d->type() != Data::PGPEncrypted + && enc_d->type() != Data::CMSEncrypted + && enc_d->type() != Data::Unknown + && enc_d->type() != Data::Invalid) + throw_(runtime_error, _f("Unsupported encryption type: %1%") % enc_d->type()); + + return enc_d; +} + +static bool is_encrypted(shared_ptr<Data> enc_d) { + if (enc_d->type() == Data::Unknown + || enc_d->type() == Data::Invalid) + return false; + else + return true; +} + +shared_ptr<Data> decrypted_stream_t::decrypt(shared_ptr<Data> enc_d) { + unique_ptr<Context> ctx; + shared_ptr<Data> dec_d; + + if (enc_d->type() == Data::Unknown + || enc_d->type() == Data::Invalid) { + ctx = nullptr; + dec_d = enc_d; + } else { +#if GPGME_VERSION_NUMBER < 0x010d00 + ctx = unique_ptr<Context>(Context::createForProtocol(enc_d->type() == Data::PGPEncrypted + ? Protocol::OpenPGP + : Protocol::CMS)); +#else + ctx = Context::create(enc_d->type() == Data::PGPEncrypted + ? Protocol::OpenPGP + : Protocol::CMS); +#endif + if (!ctx) + throw runtime_error("Unable to establish decryption context"); + + ctx->setOffline(true); + dec_d = make_shared<Data>(); + if (!dec_d) + throw runtime_error("Unable to create plain text buffer"); + + auto res = ctx->decrypt(*enc_d.get(), *dec_d.get()); + if (res.error()) + throw_(runtime_error, _f("Decryption error: %1%: %2%") % res.error().source() % res.error().asString()); + } + return dec_d; +} + +static inline void init_lib() { + auto err = GpgME::initializeLibrary(0); + if (err.code() != GPG_ERR_NO_ERROR) + throw_(runtime_error, _f("%1%: %2%") % err.source() % err.asString()); +} + +static inline void rewind(Data * d) { +#if GPGME_VERSION_NUMBER < 0x010c00 + d->seek(0, SEEK_SET); +#else + d->rewind(); +#endif +} + +istream* decrypted_stream_t::open_stream(const path& filename) { + init_lib(); + + unique_ptr<FILE, decltype(&fclose)> file(open_file(filename), &fclose); + auto enc_d = setup_cipher_buffer(file.get()); + if (is_encrypted(enc_d)) { + auto dec_d = decrypt(enc_d); + rewind(dec_d.get()); + return new decrypted_stream_t(dec_d); + } + return new ifstream(filename); +} + +decrypted_stream_t::decrypted_stream_t(path& filename) +: istream(new data_streambuffer_t(*new Data())) { + init_lib(); + + file = open_file(filename); + auto enc_d = setup_cipher_buffer(file); + dec_d = decrypt(enc_d); + rewind(dec_d.get()); + + if (is_encrypted(enc_d)) { + fclose(file); + file = nullptr; + } + + set_rdbuf(new data_streambuffer_t(*dec_d.get())); + clear(); +} + +decrypted_stream_t::decrypted_stream_t(shared_ptr<Data> dec_d) + : istream(new data_streambuffer_t(*dec_d.get())), + dec_d(dec_d), + file(nullptr) { + clear(); +} + +decrypted_stream_t::~decrypted_stream_t() { + if (file) + fclose(file); +} + +} // namespace ledger diff --git a/src/gpgme.h b/src/gpgme.h new file mode 100644 index 00000000..00824132 --- /dev/null +++ b/src/gpgme.h @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2020, Michael Raitza. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Neither the name of New Artisans LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include <system.hh> + +#include "utils.h" + +#include <streambuf> +#include <istream> + +#include <gpgme++/data.h> + +namespace ledger { + + class data_streambuffer_t : public std::streambuf { + public: + GpgME::Data& data; + + /* Size of cbuf */ + const unsigned int bufsize; + + /* Backing character buffer */ + std::unique_ptr<char[]> cbuf; + + explicit data_streambuffer_t(GpgME::Data& _data); + + virtual int_type underflow(); + + protected: + virtual std::streambuf::pos_type seekpos(std::streambuf::pos_type sp, std::ios_base::openmode which); + virtual std::streambuf::pos_type seekoff(std::streambuf::off_type off, + std::ios_base::seekdir dir, + std::ios_base::openmode which); + }; + + class decrypted_stream_t : public std::istream { + public: + std::shared_ptr<GpgME::Data> dec_d; + std::FILE * file; + + /* Establishes an istream decrypting a file pointed to by FILENAME. + + Decryption is performed at object creation and only the decrypted Data + buffer is retained as the backing store for the stream. + + Expects the input file to be unencrypted or encrypted in CMS or PGP + format (includes asymmetrically and symmetrically encrypted content). + + Calls open_file(), setup_cipher_buffer() and decrypt() and throws + exceptions noted in there on error. */ + decrypted_stream_t(path& filename); + + /* Established an istream serving the decrypted content in DEC_D. + + Make sure DEC_D is properly rewound. (Which it is not after decrypting.) + + Expects DEC_D was created by actually decrypting input data (usually a + FILE object). Otherwise GpgME just hands over the reference from the + buffer holding the "encrypted" input to DEC_D. Then, you must keep the + original object around for the lifetime of this stream. */ + decrypted_stream_t(std::shared_ptr<GpgME::Data> dec_d); + + ~decrypted_stream_t(); + + /* Opens file pointed to by FILENAME. + + Opens the file using fopen() in "rb" mode. + + Throws a runtime error when the file cannot be opened for reading. */ + static std::FILE * open_file(const path& filename); + + /* Returns a Data buffer connected to an open FILE object. + + Throws a runtime error when the content is neither PGPEncrypted, + CMSEncrypted or Unknown, or when the buffer cannot be established. */ + static std::shared_ptr<GpgME::Data> setup_cipher_buffer(std::FILE * f); + + /* Returns a Data buffer of the plain text. Decrypts cipher text by + establishing a proper decryption context, first. . + + Returns the input Data buffer when the encryption type is Unknown, which + is considered unencrypted input. + + Throws a runtime error when the decryption fails or when the cipher text + is neither PGPEncrypted nor CMSEncrypted. */ + static std::shared_ptr<GpgME::Data> decrypt(std::shared_ptr<GpgME::Data> enc_d); + + /* Returns an istream, which is either a decrypted_stream_t, given the file + is encrypted, or an ifstream object. + + Use this to create the istream! The decrypted_stream_t is perfectly + capable reading unencrypted data, but the file size and data pointers no + longer match with a standard ifstream. */ + static std::istream* open_stream(const path& filename); + }; +} diff --git a/src/system.hh.in b/src/system.hh.in index 2f0f6e79..97b2bead 100644 --- a/src/system.hh.in +++ b/src/system.hh.in @@ -68,6 +68,7 @@ #define HAVE_UNIX_PIPES @HAVE_UNIX_PIPES@ #define HAVE_BOOST_PYTHON @HAVE_BOOST_PYTHON@ +#define HAVE_GPGME @HAVE_GPGME@ #define HAVE_BOOST_REGEX_UNICODE @HAVE_BOOST_REGEX_UNICODE@ #define HAVE_BOOST_159_ISSUE_39 @HAVE_BOOST_159_ISSUE_39@ |