diff --git a/INSTALL b/INSTALL index 9c8e8e0..41049f1 100644 --- a/INSTALL +++ b/INSTALL @@ -1,23 +1,48 @@ DEPENDENCIES +To build git-crypt, you need: + + Debian/Ubuntu package RHEL/CentOS package + ----------------------------------------------------------------------------- + Make make make + A C++ compiler (e.g. gcc) g++ gcc-c++ + OpenSSL development files libssl-dev openssl-devel + + To use git-crypt, you need: - * Git 1.7.2 or newer - * OpenSSL + Debian/Ubuntu package RHEL/CentOS package + ----------------------------------------------------------------------------- + Git 1.7.2 or newer git git + OpenSSL openssl openssl -To build git-crypt, you need a C++ compiler and OpenSSL development -headers. +Note: Git 1.8.5 or newer is recommended for best performance. BUILDING GIT-CRYPT -The Makefile is tailored for g++, but should work with other compilers. +Run: $ make - $ cp git-crypt /usr/local/bin/ + $ make install -It doesn't matter where you install the git-crypt binary - choose wherever -is most convenient for you. +To install to a specific location: + + $ make install PREFIX=/usr/local + +Or, just copy the git-crypt binary to wherever is most convenient for you. + + +BUILDING THE MAN PAGE + +To build and install the git-crypt(1) man page, pass ENABLE_MAN=yes to make: + + $ make ENABLE_MAN=yes + $ make ENABLE_MAN=yes install + +xsltproc is required to build the man page. Note that xsltproc will access +the Internet to retrieve its stylesheet unless the Docbook stylesheet is +installed locally and registered in the system's XML catalog. BUILDING A DEBIAN PACKAGE diff --git a/INSTALL.md b/INSTALL.md index b4ee879..7fdb577 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,29 +1,51 @@ -Dependencies ------------- +### Dependencies + +To build git-crypt, you need: + + | Debian/Ubuntu package | RHEL/CentOS package +---------------------------|-----------------------|------------------------ +Make | make | make +A C++ compiler (e.g. gcc) | g++ | gcc-c++ +OpenSSL development files | libssl-dev | openssl-devel + To use git-crypt, you need: -* Git 1.7.2 or newer -* OpenSSL + | Debian/Ubuntu package | RHEL/CentOS package +---------------------------|-----------------------|------------------------ +Git 1.7.2 or newer | git | git +OpenSSL | openssl | openssl -To build git-crypt, you need a C++ compiler and OpenSSL development -headers. +Note: Git 1.8.5 or newer is recommended for best performance. -Building git-crypt ------------------- +### Building git-crypt -The Makefile is tailored for g++, but should work with other compilers. +Run: make - cp git-crypt /usr/local/bin/ + make install -It doesn't matter where you install the git-crypt binary - choose -wherever is most convenient for you. +To install to a specific location: + + make install PREFIX=/usr/local + +Or, just copy the git-crypt binary to wherever is most convenient for you. -Building A Debian Package -------------------------- +### Building The Man Page + +To build and install the git-crypt(1) man page, pass `ENABLE_MAN=yes` to make: + + make ENABLE_MAN=yes + make ENABLE_MAN=yes install + +xsltproc is required to build the man page. Note that xsltproc will access +the Internet to retrieve its stylesheet unless the Docbook stylesheet is +installed locally and registered in the system's XML catalog. + + +### Building A Debian Package Debian packaging can be found in the 'debian' branch of the project Git repository. The package is built using git-buildpackage as follows: @@ -32,15 +54,13 @@ repository. The package is built using git-buildpackage as follows: git-buildpackage -uc -us -Installing On Mac OS X ----------------------- +### Installing On Mac OS X Using the brew package manager, simply run: brew install git-crypt -Experimental Windows Support ----------------------------- +### Experimental Windows Support git-crypt should build on Windows with MinGW, although the build system is not yet finalized so you will need to pass your own CXX, CXXFLAGS, and diff --git a/Makefile b/Makefile index 463b502..bcc7516 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,16 @@ -CXX := c++ -CXXFLAGS := -Wall -pedantic -Wno-long-long -O2 -LDFLAGS := -PREFIX := /usr/local +# +# Copyright (c) 2015 Andrew Ayer +# +# See COPYING file for license information. +# + +CXXFLAGS ?= -Wall -pedantic -Wno-long-long -O2 +PREFIX ?= /usr/local +BINDIR ?= $(PREFIX)/bin +MANDIR ?= $(PREFIX)/share/man + +ENABLE_MAN ?= no +DOCBOOK_XSL ?= http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl OBJFILES = \ git-crypt.o \ @@ -10,22 +19,76 @@ OBJFILES = \ gpg.o \ key.o \ util.o \ - parse_options.o + parse_options.o \ + coprocess.o \ + fhstream.o OBJFILES += crypto-openssl.o LDFLAGS += -lcrypto -all: git-crypt +XSLTPROC ?= xsltproc +DOCBOOK_FLAGS += --param man.output.in.separate.dir 1 \ + --stringparam man.output.base.dir man/ \ + --param man.output.subdirs.enabled 1 \ + --param man.authors.section.enabled 1 + +all: build + +# +# Build +# +BUILD_MAN_TARGETS-yes = build-man +BUILD_MAN_TARGETS-no = +BUILD_TARGETS := build-bin $(BUILD_MAN_TARGETS-$(ENABLE_MAN)) + +build: $(BUILD_TARGETS) + +build-bin: git-crypt git-crypt: $(OBJFILES) $(CXX) $(CXXFLAGS) -o $@ $(OBJFILES) $(LDFLAGS) util.o: util.cpp util-unix.cpp util-win32.cpp +coprocess.o: coprocess.cpp coprocess-unix.cpp coprocess-win32.cpp -clean: - rm -f *.o git-crypt +build-man: man/man1/git-crypt.1 -install: git-crypt - install -m 755 git-crypt $(DESTDIR)$(PREFIX)/bin/ +man/man1/git-crypt.1: man/git-crypt.xml + $(XSLTPROC) $(DOCBOOK_FLAGS) $(DOCBOOK_XSL) $< -.PHONY: all clean install +# +# Clean +# +CLEAN_MAN_TARGETS-yes = clean-man +CLEAN_MAN_TARGETS-no = +CLEAN_TARGETS := clean-bin $(CLEAN_MAN_TARGETS-$(ENABLE_MAN)) + +clean: $(CLEAN_TARGETS) + +clean-bin: + rm -f $(OBJFILES) git-crypt + +clean-man: + rm -f man/man1/git-crypt.1 + +# +# Install +# +INSTALL_MAN_TARGETS-yes = install-man +INSTALL_MAN_TARGETS-no = +INSTALL_TARGETS := install-bin $(INSTALL_MAN_TARGETS-$(ENABLE_MAN)) + +install: $(INSTALL_TARGETS) + +install-bin: build-bin + install -d $(DESTDIR)$(BINDIR) + install -m 755 git-crypt $(DESTDIR)$(BINDIR)/ + +install-man: build-man + install -d $(DESTDIR)$(MANDIR)/man1 + install -m 644 man/man1/git-crypt.1 $(DESTDIR)$(MANDIR)/man1/ + +.PHONY: all \ + build build-bin build-man \ + clean clean-bin clean-man \ + install install-bin install-man diff --git a/NEWS b/NEWS index f51ea1f..d01a975 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,14 @@ +v0.5.0 (2015-05-30) + * Drastically speed up lock/unlock when used with Git 1.8.5 or newer. + * Add git-crypt(1) man page (pass ENABLE_MAN=yes to make to build). + * Add --trusted option to 'git-crypt gpg-add-user' to add user even if + GPG doesn't trust user's key. + * Improve 'git-crypt lock' usability, add --force option. + * Ignore symlinks and other non-files when running 'git-crypt status'. + * Fix compilation on old versions of Mac OS X. + * Fix GPG mode when with-fingerprint enabled in gpg.conf. + * Minor bug fixes and improvements to help/error messages. + v0.4.2 (2015-01-31) * Fix unlock and lock under Git 2.2.2 and higher. * Drop support for versions of Git older than 1.7.2. diff --git a/NEWS.md b/NEWS.md index 5da8f6b..dd8772f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,17 @@ News ==== +######v0.5.0 (2015-05-30) +* Drastically speed up lock/unlock when used with Git 1.8.5 or newer. +* Add git-crypt(1) man page (pass `ENABLE_MAN=yes` to make to build). +* Add --trusted option to `git-crypt gpg-add-user` to add user even if + GPG doesn't trust user's key. +* Improve `git-crypt lock` usability, add --force option. +* Ignore symlinks and other non-files when running `git-crypt status`. +* Fix compilation on old versions of Mac OS X. +* Fix GPG mode when with-fingerprint enabled in gpg.conf. +* Minor bug fixes and improvements to help/error messages. + ######v0.4.2 (2015-01-31) * Fix unlock and lock under Git 2.2.2 and higher. * Drop support for versions of Git older than 1.7.2. diff --git a/README b/README index 2d90b29..fd982ac 100644 --- a/README +++ b/README @@ -33,7 +33,10 @@ Specify files to encrypt by creating a .gitattributes file: Like a .gitignore file, it can match wildcards and should be checked into the repository. See below for more information about .gitattributes. -Make sure you don't accidentally encrypt the .gitattributes file itself! +Make sure you don't accidentally encrypt the .gitattributes file itself +(or other git files like .gitignore or .gitmodules). Make sure your +.gitattributes rules are in place *before* you add sensitive files, or +those files won't be encrypted! Share the repository with others (or with yourself) using GPG: @@ -66,7 +69,7 @@ encryption and decryption happen transparently. CURRENT STATUS -The latest version of git-crypt is 0.4.2, released on 2015-01-31. +The latest version of git-crypt is 0.5.0, released on 2015-05-30. git-crypt aims to be bug-free and reliable, meaning it shouldn't crash, malfunction, or expose your confidential data. However, it has not yet reached maturity, meaning it is not as documented, @@ -78,13 +81,13 @@ SECURITY git-crypt is more secure that other transparent git encryption systems. git-crypt encrypts files using AES-256 in CTR mode with a synthetic IV -derived from the SHA-1 HMAC of the file. This is provably semantically -secure under deterministic chosen-plaintext attack. That means that -although the encryption is deterministic (which is required so git can -distinguish when a file has and hasn't changed), it leaks no information -beyond whether two files are identical or not. Other proposals for -transparent git encryption use ECB or CBC with a fixed IV. These systems -are not semantically secure and leak information. +derived from the SHA-1 HMAC of the file. This mode of operation is +provably semantically secure under deterministic chosen-plaintext attack. +That means that although the encryption is deterministic (which is +required so git can distinguish when a file has and hasn't changed), +it leaks no information beyond whether two files are identical or not. +Other proposals for transparent git encryption use ECB or CBC with a +fixed IV. These systems are not semantically secure and leak information. LIMITATIONS @@ -98,7 +101,12 @@ need to encrypt. For encrypting an entire repository, consider using a system like git-remote-gcrypt instead. (Note: no endorsement is made of git-remote-gcrypt's security.) -git-crypt does not encrypt file names, commit messages, or other metadata. +git-crypt does not encrypt file names, commit messages, symlink targets, +gitlinks, or other metadata. + +git-crypt does not hide when a file does or doesn't change, the length +of a file, or the fact that two files are identical (see "Security" +section above). Files encrypted with git-crypt are not compressible. Even the smallest change to an encrypted file requires git to store the entire changed file, @@ -116,9 +124,9 @@ the patch itself is encrypted. To generate an encrypted patch, use `git diff --no-textconv --binary`. Alternatively, you can apply a plaintext patch outside of git using the patch command. -git-crypt does not work reliably with Atlassian SourceTree. -Files might be left in an unencrypted state. See -. +git-crypt does not work reliably with some third-party git GUIs, such +as Atlassian SourceTree +and GitHub for Mac. Files might be left in an unencrypted state. GITATTRIBUTES FILE diff --git a/README.md b/README.md index c5759c4..1aeccc8 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,10 @@ Specify files to encrypt by creating a .gitattributes file: Like a .gitignore file, it can match wildcards and should be checked into the repository. See below for more information about .gitattributes. -Make sure you don't accidentally encrypt the .gitattributes file itself! +Make sure you don't accidentally encrypt the .gitattributes file itself +(or other git files like .gitignore or .gitmodules). Make sure your +.gitattributes rules are in place *before* you add sensitive files, or +those files won't be encrypted! Share the repository with others (or with yourself) using GPG: @@ -67,8 +70,8 @@ encryption and decryption happen transparently. Current Status -------------- -The latest version of git-crypt is [0.4.2](NEWS.md), released on -2015-01-31. git-crypt aims to be bug-free and reliable, meaning it +The latest version of git-crypt is [0.5.0](NEWS.md), released on +2015-05-30. git-crypt aims to be bug-free and reliable, meaning it shouldn't crash, malfunction, or expose your confidential data. However, it has not yet reached maturity, meaning it is not as documented, featureful, or easy-to-use as it should be. Additionally, @@ -80,13 +83,13 @@ Security git-crypt is more secure that other transparent git encryption systems. git-crypt encrypts files using AES-256 in CTR mode with a synthetic IV -derived from the SHA-1 HMAC of the file. This is provably semantically -secure under deterministic chosen-plaintext attack. That means that -although the encryption is deterministic (which is required so git can -distinguish when a file has and hasn't changed), it leaks no information -beyond whether two files are identical or not. Other proposals for -transparent git encryption use ECB or CBC with a fixed IV. These -systems are not semantically secure and leak information. +derived from the SHA-1 HMAC of the file. This mode of operation is +provably semantically secure under deterministic chosen-plaintext attack. +That means that although the encryption is deterministic (which is +required so git can distinguish when a file has and hasn't changed), +it leaks no information beyond whether two files are identical or not. +Other proposals for transparent git encryption use ECB or CBC with a +fixed IV. These systems are not semantically secure and leak information. Limitations ----------- @@ -100,7 +103,12 @@ need to encrypt. For encrypting an entire repository, consider using a system like [git-remote-gcrypt](https://github.com/joeyh/git-remote-gcrypt) instead. (Note: no endorsement is made of git-remote-gcrypt's security.) -git-crypt does not encrypt file names, commit messages, or other metadata. +git-crypt does not encrypt file names, commit messages, symlink targets, +gitlinks, or other metadata. + +git-crypt does not hide when a file does or doesn't change, the length +of a file, or the fact that two files are identical (see "Security" +section above). Files encrypted with git-crypt are not compressible. Even the smallest change to an encrypted file requires git to store the entire changed file, @@ -118,9 +126,9 @@ the patch itself is encrypted. To generate an encrypted patch, use `git diff --no-textconv --binary`. Alternatively, you can apply a plaintext patch outside of git using the patch command. -git-crypt does [not work reliably with Atlassian -SourceTree](https://jira.atlassian.com/browse/SRCTREE-2511). Files might -be left in an unencrypted state. +git-crypt does not work reliably with some third-party git GUIs, such +as [Atlassian SourceTree](https://jira.atlassian.com/browse/SRCTREE-2511) +and GitHub for Mac. Files might be left in an unencrypted state. Gitattributes File ------------------ diff --git a/commands.cpp b/commands.cpp index ef40b21..2b86930 100644 --- a/commands.cpp +++ b/commands.cpp @@ -34,6 +34,7 @@ #include "key.hpp" #include "gpg.hpp" #include "parse_options.hpp" +#include "coprocess.hpp" #include #include #include @@ -60,6 +61,49 @@ static std::string attribute_name (const char* key_name) } } +static std::string git_version_string () +{ + std::vector command; + command.push_back("git"); + command.push_back("version"); + + std::stringstream output; + if (!successful_exit(exec_command(command, output))) { + throw Error("'git version' failed - is Git installed?"); + } + std::string word; + output >> word; // "git" + output >> word; // "version" + output >> word; // "1.7.10.4" + return word; +} + +static std::vector parse_version (const std::string& str) +{ + std::istringstream in(str); + std::vector version; + std::string component; + while (std::getline(in, component, '.')) { + version.push_back(std::atoi(component.c_str())); + } + return version; +} + +static const std::vector& git_version () +{ + static const std::vector version(parse_version(git_version_string())); + return version; +} + +static std::vector make_version (int a, int b, int c) +{ + std::vector version; + version.push_back(a); + version.push_back(b); + version.push_back(c); + return version; +} + static void git_config (const std::string& name, const std::string& value) { std::vector command; @@ -73,7 +117,23 @@ static void git_config (const std::string& name, const std::string& value) } } -static void git_unconfig (const std::string& name) +static bool git_has_config (const std::string& name) +{ + std::vector command; + command.push_back("git"); + command.push_back("config"); + command.push_back("--get-all"); + command.push_back(name); + + std::stringstream output; + switch (exit_status(exec_command(command, output))) { + case 0: return true; + case 1: return false; + default: throw Error("'git config' failed"); + } +} + +static void git_deconfig (const std::string& name) { std::vector command; command.push_back("git"); @@ -107,11 +167,19 @@ static void configure_git_filters (const char* key_name) } } -static void unconfigure_git_filters (const char* key_name) +static void deconfigure_git_filters (const char* key_name) { - // unconfigure the git-crypt filters - git_unconfig("filter." + attribute_name(key_name)); - git_unconfig("diff." + attribute_name(key_name)); + // deconfigure the git-crypt filters + if (git_has_config("filter." + attribute_name(key_name) + ".smudge") || + git_has_config("filter." + attribute_name(key_name) + ".clean") || + git_has_config("filter." + attribute_name(key_name) + ".required")) { + + git_deconfig("filter." + attribute_name(key_name)); + } + + if (git_has_config("diff." + attribute_name(key_name) + ".textconv")) { + git_deconfig("diff." + attribute_name(key_name)); + } } static bool git_checkout (const std::vector& paths) @@ -260,7 +328,6 @@ static void get_git_status (std::ostream& output) static std::pair get_file_attributes (const std::string& filename) { // git check-attr filter diff -- filename - // TODO: pass -z to get machine-parseable output (this requires Git 1.8.5 or higher, which was released on 27 Nov 2013) std::vector command; command.push_back("git"); command.push_back("check-attr"); @@ -309,6 +376,36 @@ static std::pair get_file_attributes (const std::strin return std::make_pair(filter_attr, diff_attr); } +// returns filter and diff attributes as a pair +static std::pair get_file_attributes (const std::string& filename, std::ostream& check_attr_stdin, std::istream& check_attr_stdout) +{ + check_attr_stdin << filename << '\0' << std::flush; + + std::string filter_attr; + std::string diff_attr; + + // Example output: + // filename\0filter\0git-crypt\0filename\0diff\0git-crypt\0 + for (int i = 0; i < 2; ++i) { + std::string filename; + std::string attr_name; + std::string attr_value; + std::getline(check_attr_stdout, filename, '\0'); + std::getline(check_attr_stdout, attr_name, '\0'); + std::getline(check_attr_stdout, attr_value, '\0'); + + if (attr_value != "unspecified" && attr_value != "unset" && attr_value != "set") { + if (attr_name == "filter") { + filter_attr = attr_value; + } else if (attr_name == "diff") { + diff_attr = attr_value; + } + } + } + + return std::make_pair(filter_attr, diff_attr); +} + static bool check_if_blob_is_encrypted (const std::string& object_id) { // git cat-file blob object_id @@ -356,31 +453,80 @@ static bool check_if_file_is_encrypted (const std::string& filename) return check_if_blob_is_encrypted(object_id); } +static bool is_git_file_mode (const std::string& mode) +{ + return (std::strtoul(mode.c_str(), NULL, 8) & 0170000) == 0100000; +} + static void get_encrypted_files (std::vector& files, const char* key_name) { // git ls-files -cz -- path_to_top - std::vector command; - command.push_back("git"); - command.push_back("ls-files"); - command.push_back("-cz"); - command.push_back("--"); + std::vector ls_files_command; + ls_files_command.push_back("git"); + ls_files_command.push_back("ls-files"); + ls_files_command.push_back("-csz"); + ls_files_command.push_back("--"); const std::string path_to_top(get_path_to_top()); if (!path_to_top.empty()) { - command.push_back(path_to_top); + ls_files_command.push_back(path_to_top); } - std::stringstream output; - if (!successful_exit(exec_command(command, output))) { + Coprocess ls_files; + std::istream* ls_files_stdout = ls_files.stdout_pipe(); + ls_files.spawn(ls_files_command); + + Coprocess check_attr; + std::ostream* check_attr_stdin = NULL; + std::istream* check_attr_stdout = NULL; + if (git_version() >= make_version(1, 8, 5)) { + // In Git 1.8.5 (released 27 Nov 2013) and higher, we use a single `git check-attr` process + // to get the attributes of all files at once. In prior versions, we have to fork and exec + // a separate `git check-attr` process for each file, since -z and --stdin aren't supported. + // In a repository with thousands of files, this results in an almost 100x speedup. + std::vector check_attr_command; + check_attr_command.push_back("git"); + check_attr_command.push_back("check-attr"); + check_attr_command.push_back("--stdin"); + check_attr_command.push_back("-z"); + check_attr_command.push_back("filter"); + check_attr_command.push_back("diff"); + + check_attr_stdin = check_attr.stdin_pipe(); + check_attr_stdout = check_attr.stdout_pipe(); + check_attr.spawn(check_attr_command); + } + + while (ls_files_stdout->peek() != -1) { + std::string mode; + std::string object_id; + std::string stage; + std::string filename; + *ls_files_stdout >> mode >> object_id >> stage >> std::ws; + std::getline(*ls_files_stdout, filename, '\0'); + + if (is_git_file_mode(mode)) { + std::string filter_attribute; + + if (check_attr_stdin) { + filter_attribute = get_file_attributes(filename, *check_attr_stdin, *check_attr_stdout).first; + } else { + filter_attribute = get_file_attributes(filename).first; + } + + if (filter_attribute == attribute_name(key_name)) { + files.push_back(filename); + } + } + } + + if (!successful_exit(ls_files.wait())) { throw Error("'git ls-files' failed - is this a Git repository?"); } - while (output.peek() != -1) { - std::string filename; - std::getline(output, filename, '\0'); - - // TODO: get file attributes en masse for efficiency... unfortunately this requires machine-parseable output from git check-attr to be workable, and this is only supported in Git 1.8.5 and above (released 27 Nov 2013) - if (get_file_attributes(filename).first == attribute_name(key_name)) { - files.push_back(filename); + if (check_attr_stdin) { + check_attr.close_stdin(); + if (!successful_exit(check_attr.wait())) { + throw Error("'git check-attr' failed - is this a Git repository?"); } } } @@ -462,7 +608,7 @@ static bool decrypt_repo_keys (std::vector& key_files, uint32_t key_ve return successful; } -static void encrypt_repo_key (const char* key_name, const Key_file::Entry& key, const std::vector& collab_keys, const std::string& keys_path, std::vector* new_files) +static void encrypt_repo_key (const char* key_name, const Key_file::Entry& key, const std::vector >& collab_keys, const std::string& keys_path, std::vector* new_files) { std::string key_file_data; { @@ -472,9 +618,11 @@ static void encrypt_repo_key (const char* key_name, const Key_file::Entry& key, key_file_data = this_version_key_file.store_to_string(); } - for (std::vector::const_iterator collab(collab_keys.begin()); collab != collab_keys.end(); ++collab) { + for (std::vector >::const_iterator collab(collab_keys.begin()); collab != collab_keys.end(); ++collab) { + const std::string& fingerprint(collab->first); + const bool key_is_trusted(collab->second); std::ostringstream path_builder; - path_builder << keys_path << '/' << (key_name ? key_name : "default") << '/' << key.version << '/' << *collab << ".gpg"; + path_builder << keys_path << '/' << (key_name ? key_name : "default") << '/' << key.version << '/' << fingerprint << ".gpg"; std::string path(path_builder.str()); if (access(path.c_str(), F_OK) == 0) { @@ -482,7 +630,7 @@ static void encrypt_repo_key (const char* key_name, const Key_file::Entry& key, } mkdir_parent(path); - gpg_encrypt_to_file(path, *collab, key_file_data.data(), key_file_data.size()); + gpg_encrypt_to_file(path, fingerprint, key_is_trusted, key_file_data.data(), key_file_data.size()); new_files->push_back(path); } } @@ -813,12 +961,7 @@ int unlock (int argc, const char** argv) return 1; } - // 2. Determine the path to the top of the repository. We pass this as the argument - // to 'git checkout' below. (Determine the path now so in case it fails we haven't already - // mucked with the git config.) - std::string path_to_top(get_path_to_top()); - - // 3. Load the key(s) + // 2. Load the key(s) std::vector key_files; if (argc > 0) { // Read from the symmetric key file(s) @@ -866,7 +1009,7 @@ int unlock (int argc, const char** argv) } - // 4. Install the key(s) and configure the git filters + // 3. Install the key(s) and configure the git filters std::vector encrypted_files; for (std::vector::iterator key_file(key_files.begin()); key_file != key_files.end(); ++key_file) { std::string internal_key_path(get_internal_key_path(key_file->get_key_name())); @@ -881,7 +1024,7 @@ int unlock (int argc, const char** argv) get_encrypted_files(encrypted_files, key_file->get_key_name()); } - // 5. Check out the files that are currently encrypted. + // 4. Check out the files that are currently encrypted. // Git won't check out a file if its mtime hasn't changed, so touch every file first. for (std::vector::const_iterator file(encrypted_files.begin()); file != encrypted_files.end(); ++file) { touch_file(*file); @@ -900,19 +1043,23 @@ void help_lock (std::ostream& out) // |--------------------------------------------------------------------------------| 80 chars out << "Usage: git-crypt lock [OPTIONS]" << std::endl; out << std::endl; - out << " -a, --all Lock all keys, instead of just the default" << std::endl; - out << " -k, --key-name KEYNAME Lock the given key, instead of the default" << std::endl; + out << " -a, --all Lock all keys, instead of just the default" << std::endl; + out << " -k, --key-name KEYNAME Lock the given key, instead of the default" << std::endl; + out << " -f, --force Lock even if unclean (you may lose uncommited work)" << std::endl; out << std::endl; } int lock (int argc, const char** argv) { const char* key_name = 0; - bool all_keys = false; + bool all_keys = false; + bool force = false; Options_list options; options.push_back(Option_def("-k", &key_name)); options.push_back(Option_def("--key-name", &key_name)); options.push_back(Option_def("-a", &all_keys)); options.push_back(Option_def("--all", &all_keys)); + options.push_back(Option_def("-f", &force)); + options.push_back(Option_def("--force", &force)); int argi = parse_options(options, argc, argv); @@ -936,34 +1083,30 @@ int lock (int argc, const char** argv) std::stringstream status_output; get_git_status(status_output); - if (status_output.peek() != -1) { + if (!force && status_output.peek() != -1) { std::clog << "Error: Working directory not clean." << std::endl; std::clog << "Please commit your changes or 'git stash' them before running 'git-crypt lock'." << std::endl; + std::clog << "Or, use 'git-crypt lock --force' and possibly lose uncommitted changes." << std::endl; return 1; } - // 2. Determine the path to the top of the repository. We pass this as the argument - // to 'git checkout' below. (Determine the path now so in case it fails we haven't already - // mucked with the git config.) - std::string path_to_top(get_path_to_top()); - - // 3. unconfigure the git filters and remove decrypted keys + // 2. deconfigure the git filters and remove decrypted keys std::vector encrypted_files; if (all_keys) { - // unconfigure for all keys + // deconfigure for all keys std::vector dirents = get_directory_contents(get_internal_keys_path().c_str()); for (std::vector::const_iterator dirent(dirents.begin()); dirent != dirents.end(); ++dirent) { const char* this_key_name = (*dirent == "default" ? 0 : dirent->c_str()); remove_file(get_internal_key_path(this_key_name)); - unconfigure_git_filters(this_key_name); + deconfigure_git_filters(this_key_name); get_encrypted_files(encrypted_files, this_key_name); } } else { // just handle the given key std::string internal_key_path(get_internal_key_path(key_name)); if (access(internal_key_path.c_str(), F_OK) == -1 && errno == ENOENT) { - std::clog << "Error: this repository is not currently locked"; + std::clog << "Error: this repository is already locked"; if (key_name) { std::clog << " with key '" << key_name << "'"; } @@ -972,11 +1115,11 @@ int lock (int argc, const char** argv) } remove_file(internal_key_path); - unconfigure_git_filters(key_name); + deconfigure_git_filters(key_name); get_encrypted_files(encrypted_files, key_name); } - // 4. Check out the files that are currently decrypted but should be encrypted. + // 3. Check out the files that are currently decrypted but should be encrypted. // Git won't check out a file if its mtime hasn't changed, so touch every file first. for (std::vector::const_iterator file(encrypted_files.begin()); file != encrypted_files.end(); ++file) { touch_file(*file); @@ -997,17 +1140,20 @@ void help_add_gpg_user (std::ostream& out) out << std::endl; out << " -k, --key-name KEYNAME Add GPG user to given key, instead of default" << std::endl; out << " -n, --no-commit Don't automatically commit" << std::endl; + out << " --trusted Assume the GPG user IDs are trusted" << std::endl; out << std::endl; } int add_gpg_user (int argc, const char** argv) { const char* key_name = 0; bool no_commit = false; + bool trusted = false; Options_list options; options.push_back(Option_def("-k", &key_name)); options.push_back(Option_def("--key-name", &key_name)); options.push_back(Option_def("-n", &no_commit)); options.push_back(Option_def("--no-commit", &no_commit)); + options.push_back(Option_def("--trusted", &trusted)); int argi = parse_options(options, argc, argv); if (argc - argi == 0) { @@ -1016,8 +1162,8 @@ int add_gpg_user (int argc, const char** argv) return 2; } - // build a list of key fingerprints for every collaborator specified on the command line - std::vector collab_keys; + // build a list of key fingerprints, and whether the key is trusted, for every collaborator specified on the command line + std::vector > collab_keys; for (int i = argi; i < argc; ++i) { std::vector keys(gpg_lookup_key(argv[i])); @@ -1029,7 +1175,9 @@ int add_gpg_user (int argc, const char** argv) std::clog << "Error: more than one public key matches '" << argv[i] << "' - please be more specific" << std::endl; return 1; } - collab_keys.push_back(keys[0]); + + const bool is_full_fingerprint(std::strncmp(argv[i], "0x", 2) == 0 && std::strlen(argv[i]) == 42); + collab_keys.push_back(std::make_pair(keys[0], trusted || is_full_fingerprint)); } // TODO: have a retroactive option to grant access to all key versions, not just the most recent @@ -1050,6 +1198,9 @@ int add_gpg_user (int argc, const char** argv) const std::string state_gitattributes_path(state_path + "/.gitattributes"); if (access(state_gitattributes_path.c_str(), F_OK) != 0) { std::ofstream state_gitattributes_file(state_gitattributes_path.c_str()); + // |--------------------------------------------------------------------------------| 80 chars + state_gitattributes_file << "# Do not edit this file. To specify the files to encrypt, create your own\n"; + state_gitattributes_file << "# .gitattributes file in the directory where your files are.\n"; state_gitattributes_file << "* !filter !diff\n"; state_gitattributes_file.close(); if (!state_gitattributes_file) { @@ -1077,8 +1228,8 @@ int add_gpg_user (int argc, const char** argv) // TODO: include key_name in commit message std::ostringstream commit_message_builder; commit_message_builder << "Add " << collab_keys.size() << " git-crypt collaborator" << (collab_keys.size() != 1 ? "s" : "") << "\n\nNew collaborators:\n\n"; - for (std::vector::const_iterator collab(collab_keys.begin()); collab != collab_keys.end(); ++collab) { - commit_message_builder << '\t' << gpg_shorten_fingerprint(*collab) << ' ' << gpg_get_uid(*collab) << '\n'; + for (std::vector >::const_iterator collab(collab_keys.begin()); collab != collab_keys.end(); ++collab) { + commit_message_builder << '\t' << gpg_shorten_fingerprint(collab->first) << ' ' << gpg_get_uid(collab->first) << '\n'; } // git commit -m MESSAGE NEW_FILE ... @@ -1398,6 +1549,9 @@ int status (int argc, const char** argv) std::string mode; std::string stage; output >> mode >> object_id >> stage; + if (!is_git_file_mode(mode)) { + continue; + } } output >> std::ws; std::getline(output, filename, '\0'); diff --git a/coprocess-unix.cpp b/coprocess-unix.cpp new file mode 100644 index 0000000..f9577e5 --- /dev/null +++ b/coprocess-unix.cpp @@ -0,0 +1,186 @@ +/* + * Copyright 2015 Andrew Ayer + * + * This file is part of git-crypt. + * + * git-crypt 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. + * + * git-crypt 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 git-crypt. If not, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify the Program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, the licensors of the Program + * grant you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#include "coprocess.hpp" +#include "util.hpp" +#include +#include +#include + +static int execvp (const std::string& file, const std::vector& args) +{ + std::vector args_c_str; + args_c_str.reserve(args.size()); + for (std::vector::const_iterator arg(args.begin()); arg != args.end(); ++arg) { + args_c_str.push_back(arg->c_str()); + } + args_c_str.push_back(NULL); + return execvp(file.c_str(), const_cast(&args_c_str[0])); +} + +Coprocess::Coprocess () +{ + pid = -1; + stdin_pipe_reader = -1; + stdin_pipe_writer = -1; + stdin_pipe_ostream = NULL; + stdout_pipe_reader = -1; + stdout_pipe_writer = -1; + stdout_pipe_istream = NULL; +} + +Coprocess::~Coprocess () +{ + close_stdin(); + close_stdout(); +} + +std::ostream* Coprocess::stdin_pipe () +{ + if (!stdin_pipe_ostream) { + int fds[2]; + if (pipe(fds) == -1) { + throw System_error("pipe", "", errno); + } + stdin_pipe_reader = fds[0]; + stdin_pipe_writer = fds[1]; + stdin_pipe_ostream = new ofhstream(this, write_stdin); + } + return stdin_pipe_ostream; +} + +void Coprocess::close_stdin () +{ + delete stdin_pipe_ostream; + stdin_pipe_ostream = NULL; + if (stdin_pipe_writer != -1) { + close(stdin_pipe_writer); + stdin_pipe_writer = -1; + } + if (stdin_pipe_reader != -1) { + close(stdin_pipe_reader); + stdin_pipe_reader = -1; + } +} + +std::istream* Coprocess::stdout_pipe () +{ + if (!stdout_pipe_istream) { + int fds[2]; + if (pipe(fds) == -1) { + throw System_error("pipe", "", errno); + } + stdout_pipe_reader = fds[0]; + stdout_pipe_writer = fds[1]; + stdout_pipe_istream = new ifhstream(this, read_stdout); + } + return stdout_pipe_istream; +} + +void Coprocess::close_stdout () +{ + delete stdout_pipe_istream; + stdout_pipe_istream = NULL; + if (stdout_pipe_writer != -1) { + close(stdout_pipe_writer); + stdout_pipe_writer = -1; + } + if (stdout_pipe_reader != -1) { + close(stdout_pipe_reader); + stdout_pipe_reader = -1; + } +} + +void Coprocess::spawn (const std::vector& args) +{ + pid = fork(); + if (pid == -1) { + throw System_error("fork", "", errno); + } + if (pid == 0) { + if (stdin_pipe_writer != -1) { + close(stdin_pipe_writer); + } + if (stdout_pipe_reader != -1) { + close(stdout_pipe_reader); + } + if (stdin_pipe_reader != -1) { + dup2(stdin_pipe_reader, 0); + close(stdin_pipe_reader); + } + if (stdout_pipe_writer != -1) { + dup2(stdout_pipe_writer, 1); + close(stdout_pipe_writer); + } + + execvp(args[0], args); + perror(args[0].c_str()); + _exit(-1); + } + if (stdin_pipe_reader != -1) { + close(stdin_pipe_reader); + stdin_pipe_reader = -1; + } + if (stdout_pipe_writer != -1) { + close(stdout_pipe_writer); + stdout_pipe_writer = -1; + } +} + +int Coprocess::wait () +{ + int status = 0; + if (waitpid(pid, &status, 0) == -1) { + throw System_error("waitpid", "", errno); + } + return status; +} + +size_t Coprocess::write_stdin (void* handle, const void* buf, size_t count) +{ + const int fd = static_cast(handle)->stdin_pipe_writer; + ssize_t ret; + while ((ret = write(fd, buf, count)) == -1 && errno == EINTR); // restart if interrupted + if (ret < 0) { + throw System_error("write", "", errno); + } + return ret; +} + +size_t Coprocess::read_stdout (void* handle, void* buf, size_t count) +{ + const int fd = static_cast(handle)->stdout_pipe_reader; + ssize_t ret; + while ((ret = read(fd, buf, count)) == -1 && errno == EINTR); // restart if interrupted + if (ret < 0) { + throw System_error("read", "", errno); + } + return ret; +} diff --git a/coprocess-unix.hpp b/coprocess-unix.hpp new file mode 100644 index 0000000..f50f808 --- /dev/null +++ b/coprocess-unix.hpp @@ -0,0 +1,68 @@ +/* + * Copyright 2015 Andrew Ayer + * + * This file is part of git-crypt. + * + * git-crypt 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. + * + * git-crypt 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 git-crypt. If not, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify the Program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, the licensors of the Program + * grant you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#ifndef GIT_CRYPT_COPROCESS_HPP +#define GIT_CRYPT_COPROCESS_HPP + +#include "fhstream.hpp" +#include +#include + +class Coprocess { + pid_t pid; + + int stdin_pipe_reader; + int stdin_pipe_writer; + ofhstream* stdin_pipe_ostream; + static size_t write_stdin (void*, const void*, size_t); + + int stdout_pipe_reader; + int stdout_pipe_writer; + ifhstream* stdout_pipe_istream; + static size_t read_stdout (void*, void*, size_t); + + Coprocess (const Coprocess&); // Disallow copy + Coprocess& operator= (const Coprocess&); // Disallow assignment +public: + Coprocess (); + ~Coprocess (); + + std::ostream* stdin_pipe (); + void close_stdin (); + + std::istream* stdout_pipe (); + void close_stdout (); + + void spawn (const std::vector&); + + int wait (); +}; + +#endif diff --git a/coprocess-win32.cpp b/coprocess-win32.cpp new file mode 100644 index 0000000..46e21d0 --- /dev/null +++ b/coprocess-win32.cpp @@ -0,0 +1,269 @@ +/* + * Copyright 2015 Andrew Ayer + * + * This file is part of git-crypt. + * + * git-crypt 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. + * + * git-crypt 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 git-crypt. If not, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify the Program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, the licensors of the Program + * grant you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#include "coprocess-win32.hpp" +#include "util.hpp" + + +static void escape_cmdline_argument (std::string& cmdline, const std::string& arg) +{ + // For an explanation of Win32's arcane argument quoting rules, see: + // http://msdn.microsoft.com/en-us/library/17w5ykft%28v=vs.85%29.aspx + // http://msdn.microsoft.com/en-us/library/bb776391%28v=vs.85%29.aspx + // http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx + // http://blogs.msdn.com/b/oldnewthing/archive/2010/09/17/10063629.aspx + cmdline.push_back('"'); + + std::string::const_iterator p(arg.begin()); + while (p != arg.end()) { + if (*p == '"') { + cmdline.push_back('\\'); + cmdline.push_back('"'); + ++p; + } else if (*p == '\\') { + unsigned int num_backslashes = 0; + while (p != arg.end() && *p == '\\') { + ++num_backslashes; + ++p; + } + if (p == arg.end() || *p == '"') { + // Backslashes need to be escaped + num_backslashes *= 2; + } + while (num_backslashes--) { + cmdline.push_back('\\'); + } + } else { + cmdline.push_back(*p++); + } + } + + cmdline.push_back('"'); +} + +static std::string format_cmdline (const std::vector& command) +{ + std::string cmdline; + for (std::vector::const_iterator arg(command.begin()); arg != command.end(); ++arg) { + if (arg != command.begin()) { + cmdline.push_back(' '); + } + escape_cmdline_argument(cmdline, *arg); + } + return cmdline; +} + +static HANDLE spawn_command (const std::vector& command, HANDLE stdin_handle, HANDLE stdout_handle, HANDLE stderr_handle) +{ + PROCESS_INFORMATION proc_info; + ZeroMemory(&proc_info, sizeof(proc_info)); + + STARTUPINFO start_info; + ZeroMemory(&start_info, sizeof(start_info)); + + start_info.cb = sizeof(STARTUPINFO); + start_info.hStdInput = stdin_handle ? stdin_handle : GetStdHandle(STD_INPUT_HANDLE); + start_info.hStdOutput = stdout_handle ? stdout_handle : GetStdHandle(STD_OUTPUT_HANDLE); + start_info.hStdError = stderr_handle ? stderr_handle : GetStdHandle(STD_ERROR_HANDLE); + start_info.dwFlags |= STARTF_USESTDHANDLES; + + std::string cmdline(format_cmdline(command)); + + if (!CreateProcessA(NULL, // application name (NULL to use command line) + const_cast(cmdline.c_str()), + NULL, // process security attributes + NULL, // primary thread security attributes + TRUE, // handles are inherited + 0, // creation flags + NULL, // use parent's environment + NULL, // use parent's current directory + &start_info, + &proc_info)) { + throw System_error("CreateProcess", cmdline, GetLastError()); + } + + CloseHandle(proc_info.hThread); + + return proc_info.hProcess; +} + + +Coprocess::Coprocess () +{ + proc_handle = NULL; + stdin_pipe_reader = NULL; + stdin_pipe_writer = NULL; + stdin_pipe_ostream = NULL; + stdout_pipe_reader = NULL; + stdout_pipe_writer = NULL; + stdout_pipe_istream = NULL; +} + +Coprocess::~Coprocess () +{ + close_stdin(); + close_stdout(); + if (proc_handle) { + CloseHandle(proc_handle); + } +} + +std::ostream* Coprocess::stdin_pipe () +{ + if (!stdin_pipe_ostream) { + SECURITY_ATTRIBUTES sec_attr; + + // Set the bInheritHandle flag so pipe handles are inherited. + sec_attr.nLength = sizeof(SECURITY_ATTRIBUTES); + sec_attr.bInheritHandle = TRUE; + sec_attr.lpSecurityDescriptor = NULL; + + // Create a pipe for the child process's STDIN. + if (!CreatePipe(&stdin_pipe_reader, &stdin_pipe_writer, &sec_attr, 0)) { + throw System_error("CreatePipe", "", GetLastError()); + } + + // Ensure the write handle to the pipe for STDIN is not inherited. + if (!SetHandleInformation(stdin_pipe_writer, HANDLE_FLAG_INHERIT, 0)) { + throw System_error("SetHandleInformation", "", GetLastError()); + } + + stdin_pipe_ostream = new ofhstream(this, write_stdin); + } + return stdin_pipe_ostream; +} + +void Coprocess::close_stdin () +{ + delete stdin_pipe_ostream; + stdin_pipe_ostream = NULL; + if (stdin_pipe_writer) { + CloseHandle(stdin_pipe_writer); + stdin_pipe_writer = NULL; + } + if (stdin_pipe_reader) { + CloseHandle(stdin_pipe_reader); + stdin_pipe_reader = NULL; + } +} + +std::istream* Coprocess::stdout_pipe () +{ + if (!stdout_pipe_istream) { + SECURITY_ATTRIBUTES sec_attr; + + // Set the bInheritHandle flag so pipe handles are inherited. + sec_attr.nLength = sizeof(SECURITY_ATTRIBUTES); + sec_attr.bInheritHandle = TRUE; + sec_attr.lpSecurityDescriptor = NULL; + + // Create a pipe for the child process's STDOUT. + if (!CreatePipe(&stdout_pipe_reader, &stdout_pipe_writer, &sec_attr, 0)) { + throw System_error("CreatePipe", "", GetLastError()); + } + + // Ensure the read handle to the pipe for STDOUT is not inherited. + if (!SetHandleInformation(stdout_pipe_reader, HANDLE_FLAG_INHERIT, 0)) { + throw System_error("SetHandleInformation", "", GetLastError()); + } + + stdout_pipe_istream = new ifhstream(this, read_stdout); + } + return stdout_pipe_istream; +} + +void Coprocess::close_stdout () +{ + delete stdout_pipe_istream; + stdout_pipe_istream = NULL; + if (stdout_pipe_writer) { + CloseHandle(stdout_pipe_writer); + stdout_pipe_writer = NULL; + } + if (stdout_pipe_reader) { + CloseHandle(stdout_pipe_reader); + stdout_pipe_reader = NULL; + } +} + +void Coprocess::spawn (const std::vector& args) +{ + proc_handle = spawn_command(args, stdin_pipe_reader, stdout_pipe_writer, NULL); + if (stdin_pipe_reader) { + CloseHandle(stdin_pipe_reader); + stdin_pipe_reader = NULL; + } + if (stdout_pipe_writer) { + CloseHandle(stdout_pipe_writer); + stdout_pipe_writer = NULL; + } +} + +int Coprocess::wait () +{ + if (WaitForSingleObject(proc_handle, INFINITE) == WAIT_FAILED) { + throw System_error("WaitForSingleObject", "", GetLastError()); + } + + DWORD exit_code; + if (!GetExitCodeProcess(proc_handle, &exit_code)) { + throw System_error("GetExitCodeProcess", "", GetLastError()); + } + + return exit_code; +} + +size_t Coprocess::write_stdin (void* handle, const void* buf, size_t count) +{ + DWORD bytes_written; + if (!WriteFile(static_cast(handle)->stdin_pipe_writer, buf, count, &bytes_written, NULL)) { + throw System_error("WriteFile", "", GetLastError()); + } + return bytes_written; +} + +size_t Coprocess::read_stdout (void* handle, void* buf, size_t count) +{ + // Note that ReadFile on a pipe may return with bytes_read==0 if the other + // end of the pipe writes zero bytes, so retry when this happens. + // When the other end of the pipe actually closes, ReadFile + // fails with ERROR_BROKEN_PIPE. + DWORD bytes_read; + do { + if (!ReadFile(static_cast(handle)->stdout_pipe_reader, buf, count, &bytes_read, NULL)) { + const DWORD read_error = GetLastError(); + if (read_error != ERROR_BROKEN_PIPE) { + throw System_error("ReadFile", "", read_error); + } + return 0; + } + } while (bytes_read == 0); + return bytes_read; +} diff --git a/coprocess-win32.hpp b/coprocess-win32.hpp new file mode 100644 index 0000000..532728d --- /dev/null +++ b/coprocess-win32.hpp @@ -0,0 +1,68 @@ +/* + * Copyright 2015 Andrew Ayer + * + * This file is part of git-crypt. + * + * git-crypt 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. + * + * git-crypt 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 git-crypt. If not, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify the Program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, the licensors of the Program + * grant you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#ifndef GIT_CRYPT_COPROCESS_HPP +#define GIT_CRYPT_COPROCESS_HPP + +#include "fhstream.hpp" +#include +#include + +class Coprocess { + HANDLE proc_handle; + + HANDLE stdin_pipe_reader; + HANDLE stdin_pipe_writer; + ofhstream* stdin_pipe_ostream; + static size_t write_stdin (void*, const void*, size_t); + + HANDLE stdout_pipe_reader; + HANDLE stdout_pipe_writer; + ifhstream* stdout_pipe_istream; + static size_t read_stdout (void*, void*, size_t); + + Coprocess (const Coprocess&); // Disallow copy + Coprocess& operator= (const Coprocess&); // Disallow assignment +public: + Coprocess (); + ~Coprocess (); + + std::ostream* stdin_pipe (); + void close_stdin (); + + std::istream* stdout_pipe (); + void close_stdout (); + + void spawn (const std::vector&); + + int wait (); +}; + +#endif diff --git a/coprocess.cpp b/coprocess.cpp new file mode 100644 index 0000000..813cc2a --- /dev/null +++ b/coprocess.cpp @@ -0,0 +1,5 @@ +#ifdef _WIN32 +#include "coprocess-win32.cpp" +#else +#include "coprocess-unix.cpp" +#endif diff --git a/coprocess.hpp b/coprocess.hpp new file mode 100644 index 0000000..f8ac370 --- /dev/null +++ b/coprocess.hpp @@ -0,0 +1,5 @@ +#ifdef _WIN32 +#include "coprocess-win32.hpp" +#else +#include "coprocess-unix.hpp" +#endif diff --git a/fhstream.cpp b/fhstream.cpp new file mode 100644 index 0000000..8c8fc79 --- /dev/null +++ b/fhstream.cpp @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2012, 2015 Andrew Ayer + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * Except as contained in this notice, the name(s) of the above copyright + * holders shall not be used in advertising or otherwise to promote the + * sale, use or other dealings in this Software without prior written + * authorization. + */ + +#include +#include // for std::min + +#include "fhstream.hpp" + +/* + * ofhstream + */ + +ofhbuf::ofhbuf (void* arg_handle, size_t (*arg_write_fun)(void*, const void*, size_t)) +: handle(arg_handle), + write_fun(arg_write_fun), + buffer(new char[default_buffer_size]), + buffer_size(default_buffer_size) +{ + reset_buffer(); +} + +ofhbuf::~ofhbuf () +{ + if (handle) { + try { + sync(); + } catch (...) { + // Ignore exception since we're in the destructor. + // To catch write errors, call sync() explicitly. + } + } + delete[] buffer; +} + +ofhbuf::int_type ofhbuf::overflow (ofhbuf::int_type c) +{ + const char* p = pbase(); + std::streamsize bytes_to_write = pptr() - p; + + if (!is_eof(c)) { + *pptr() = c; + ++bytes_to_write; + } + + while (bytes_to_write > 0) { + const size_t bytes_written = write_fun(handle, p, bytes_to_write); + bytes_to_write -= bytes_written; + p += bytes_written; + } + + reset_buffer(); + + return traits_type::to_int_type(0); +} + +int ofhbuf::sync () +{ + return !is_eof(overflow(traits_type::eof())) ? 0 : -1; +} + +std::streamsize ofhbuf::xsputn (const char* s, std::streamsize n) +{ + // Use heuristic to decide whether to write directly or just use buffer + // Write directly only if n >= MIN(4096, available buffer capacity) + // (this is similar to what basic_filebuf does) + + if (n < std::min(4096, epptr() - pptr())) { + // Not worth it to do a direct write + return std::streambuf::xsputn(s, n); + } + + // Before we can do a direct write of this string, we need to flush + // out the current contents of the buffer. + if (pbase() != pptr()) { + overflow(traits_type::eof()); // throws an exception or it succeeds + } + + // Now we can go ahead and write out the string. + size_t bytes_to_write = n; + + while (bytes_to_write > 0) { + const size_t bytes_written = write_fun(handle, s, bytes_to_write); + bytes_to_write -= bytes_written; + s += bytes_written; + } + + return n; // Return the total bytes written +} + +std::streambuf* ofhbuf::setbuf (char* s, std::streamsize n) +{ + if (s == 0 && n == 0) { + // Switch to unbuffered + // This won't take effect until the next overflow or sync + // (We defer it taking effect so that write errors can be properly reported) + // To cause it to take effect as soon as possible, we artificially reduce the + // size of the buffer so it has no space left. This will trigger an overflow + // on the next put. + std::streambuf::setp(pbase(), pptr()); + std::streambuf::pbump(pptr() - pbase()); + buffer_size = 1; + } + return this; +} + + + +/* + * ifhstream + */ + +ifhbuf::ifhbuf (void* arg_handle, size_t (*arg_read_fun)(void*, void*, size_t)) +: handle(arg_handle), + read_fun(arg_read_fun), + buffer(new char[default_buffer_size + putback_size]), + buffer_size(default_buffer_size) +{ + reset_buffer(0, 0); +} + +ifhbuf::~ifhbuf () +{ + delete[] buffer; +} + +ifhbuf::int_type ifhbuf::underflow () +{ + if (gptr() >= egptr()) { // A true underflow (no bytes in buffer left to read) + + // Move the putback_size most-recently-read characters into the putback area + size_t nputback = std::min(gptr() - eback(), putback_size); + std::memmove(buffer + (putback_size - nputback), gptr() - nputback, nputback); + + // Now read new characters from the file descriptor + const size_t nread = read_fun(handle, buffer + putback_size, buffer_size); + if (nread == 0) { + // EOF + return traits_type::eof(); + } + + // Reset the buffer + reset_buffer(nputback, nread); + } + + // Return the next character + return traits_type::to_int_type(*gptr()); +} + +std::streamsize ifhbuf::xsgetn (char* s, std::streamsize n) +{ + // Use heuristic to decide whether to read directly + // Read directly only if n >= bytes_available + 4096 + + std::streamsize bytes_available = egptr() - gptr(); + + if (n < bytes_available + 4096) { + // Not worth it to do a direct read + return std::streambuf::xsgetn(s, n); + } + + std::streamsize total_bytes_read = 0; + + // First, copy out the bytes currently in the buffer + std::memcpy(s, gptr(), bytes_available); + + s += bytes_available; + n -= bytes_available; + total_bytes_read += bytes_available; + + // Now do the direct read + while (n > 0) { + const size_t bytes_read = read_fun(handle, s, n); + if (bytes_read == 0) { + // EOF + break; + } + + s += bytes_read; + n -= bytes_read; + total_bytes_read += bytes_read; + } + + // Fill up the putback area with the most recently read characters + size_t nputback = std::min(total_bytes_read, putback_size); + std::memcpy(buffer + (putback_size - nputback), s - nputback, nputback); + + // Reset the buffer with no bytes available for reading, but with some putback characters + reset_buffer(nputback, 0); + + // Return the total number of bytes read + return total_bytes_read; +} + +std::streambuf* ifhbuf::setbuf (char* s, std::streamsize n) +{ + if (s == 0 && n == 0) { + // Switch to unbuffered + // This won't take effect until the next underflow (we don't want to + // lose what's currently in the buffer!) + buffer_size = 1; + } + return this; +} diff --git a/fhstream.hpp b/fhstream.hpp new file mode 100644 index 0000000..28bb1c7 --- /dev/null +++ b/fhstream.hpp @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2012, 2015 Andrew Ayer + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * Except as contained in this notice, the name(s) of the above copyright + * holders shall not be used in advertising or otherwise to promote the + * sale, use or other dealings in this Software without prior written + * authorization. + */ + +#ifndef GIT_CRYPT_FHSTREAM_HPP +#define GIT_CRYPT_FHSTREAM_HPP + +#include +#include +#include + +/* + * ofhstream + */ +class ofhbuf : public std::streambuf { + enum { default_buffer_size = 8192 }; + + void* handle; + size_t (*write_fun)(void*, const void*, size_t); + char* buffer; + size_t buffer_size; + + inline void reset_buffer () + { + std::streambuf::setp(buffer, buffer + buffer_size - 1); + } + static inline bool is_eof (int_type ch) { return traits_type::eq_int_type(ch, traits_type::eof()); } + + // Disallow copy +#if __cplusplus >= 201103L /* C++11 */ + ofhbuf (const ofhbuf&) = delete; + ofhbuf& operator= (const ofhbuf&) = delete; +#else + ofhbuf (const ofhbuf&); + ofhbuf& operator= (const ofhbuf&); +#endif + +protected: + virtual int_type overflow (int_type ch =traits_type::eof()); + virtual int sync (); + virtual std::streamsize xsputn (const char*, std::streamsize); + virtual std::streambuf* setbuf (char*, std::streamsize); + +public: + ofhbuf (void*, size_t (*)(void*, const void*, size_t)); + ~ofhbuf (); // WARNING: calls sync() and ignores exceptions +}; + +class ofhstream : public std::ostream { + mutable ofhbuf buf; +public: + ofhstream (void* handle, size_t (*write_fun)(void*, const void*, size_t)) + : std::ostream(0), buf(handle, write_fun) + { + std::ostream::rdbuf(&buf); + } + + ofhbuf* rdbuf () const { return &buf; } +}; + + +/* + * ifhstream + */ +class ifhbuf : public std::streambuf { + enum { + default_buffer_size = 8192, + putback_size = 4 + }; + + void* handle; + size_t (*read_fun)(void*, void*, size_t); + char* buffer; + size_t buffer_size; + + inline void reset_buffer (size_t nputback, size_t nread) + { + std::streambuf::setg(buffer + (putback_size - nputback), buffer + putback_size, buffer + putback_size + nread); + } + // Disallow copy +#if __cplusplus >= 201103L /* C++11 */ + ifhbuf (const ifhbuf&) = delete; + ifhbuf& operator= (const ifhbuf&) = delete; +#else + ifhbuf (const ifhbuf&); + ifhbuf& operator= (const ifhbuf&); +#endif + +protected: + virtual int_type underflow (); + virtual std::streamsize xsgetn (char*, std::streamsize); + virtual std::streambuf* setbuf (char*, std::streamsize); + +public: + ifhbuf (void*, size_t (*)(void*, void*, size_t)); + ~ifhbuf (); // Can't fail +}; + +class ifhstream : public std::istream { + mutable ifhbuf buf; +public: + explicit ifhstream (void* handle, size_t (*read_fun)(void*, void*, size_t)) + : std::istream(0), buf(handle, read_fun) + { + std::istream::rdbuf(&buf); + } + + ifhbuf* rdbuf () const { return &buf; } +}; + +#endif diff --git a/git-crypt.hpp b/git-crypt.hpp index 78b8196..bb4beaa 100644 --- a/git-crypt.hpp +++ b/git-crypt.hpp @@ -31,7 +31,7 @@ #ifndef GIT_CRYPT_GIT_CRYPT_HPP #define GIT_CRYPT_GIT_CRYPT_HPP -#define VERSION "0.4.2" +#define VERSION "0.5.0" extern const char* argv0; // initialized in main() to argv[0] diff --git a/gpg.cpp b/gpg.cpp index 4813b35..04f3f60 100644 --- a/gpg.cpp +++ b/gpg.cpp @@ -102,10 +102,15 @@ std::vector gpg_lookup_key (const std::string& query) command.push_back(query); std::stringstream command_output; if (successful_exit(exec_command(command, command_output))) { + bool is_pubkey = false; while (command_output.peek() != -1) { std::string line; std::getline(command_output, line); - if (line.substr(0, 4) == "fpr:") { + if (line.substr(0, 4) == "pub:") { + is_pubkey = true; + } else if (line.substr(0, 4) == "sub:") { + is_pubkey = false; + } else if (is_pubkey && line.substr(0, 4) == "fpr:") { // fpr:::::::::7A399B2DB06D039020CD1CE1D0F3702D61489532: // want the 9th column (counting from 0) fingerprints.push_back(gpg_nth_column(line, 9)); @@ -145,12 +150,16 @@ std::vector gpg_list_secret_keys () return secret_keys; } -void gpg_encrypt_to_file (const std::string& filename, const std::string& recipient_fingerprint, const char* p, size_t len) +void gpg_encrypt_to_file (const std::string& filename, const std::string& recipient_fingerprint, bool key_is_trusted, const char* p, size_t len) { // gpg --batch -o FILENAME -r RECIPIENT -e std::vector command; command.push_back("gpg"); command.push_back("--batch"); + if (key_is_trusted) { + command.push_back("--trust-model"); + command.push_back("always"); + } command.push_back("-o"); command.push_back(filename); command.push_back("-r"); diff --git a/gpg.hpp b/gpg.hpp index cd55171..77997b1 100644 --- a/gpg.hpp +++ b/gpg.hpp @@ -45,7 +45,7 @@ std::string gpg_shorten_fingerprint (const std::string& fingerprint); std::string gpg_get_uid (const std::string& fingerprint); std::vector gpg_lookup_key (const std::string& query); std::vector gpg_list_secret_keys (); -void gpg_encrypt_to_file (const std::string& filename, const std::string& recipient_fingerprint, const char* p, size_t len); +void gpg_encrypt_to_file (const std::string& filename, const std::string& recipient_fingerprint, bool key_is_trusted, const char* p, size_t len); void gpg_decrypt_from_file (const std::string& filename, std::ostream&); #endif diff --git a/man/git-crypt.xml b/man/git-crypt.xml new file mode 100644 index 0000000..7a20569 --- /dev/null +++ b/man/git-crypt.xml @@ -0,0 +1,493 @@ + + + + + git-crypt + 2015-05-30 + git-crypt 0.5.0 + + + Andrew Ayer + + agwa@andrewayer.name + https://www.agwa.name + + + + + git-crypt + 1 + + + + git-crypt + transparent file encryption in Git + + + + + git-crypt OPTIONS COMMAND ARGS + + + + + Common commands + + git-crypt init + + + git-crypt status + + + git-crypt lock + + + + GPG commands + + git-crypt add-gpg-user GPG_USER_ID + + + git-crypt unlock + + + + Symmetric key commands + + git-crypt export-key OUTPUT_KEY_FILE + + + git-crypt unlock KEY_FILE + + + + + Description + + + git-crypt enables transparent encryption and decryption + of files in a git repository. Files which you choose to protect are encrypted when committed, + and decrypted when checked out. git-crypt lets you freely share a repository containing a mix of + public and private content. git-crypt gracefully degrades, so developers without the secret key + can still clone and commit to a repository with encrypted files. This lets you store your secret + material (such as keys or passwords) in the same repository as your code, without requiring you + to lock down your entire repository. + + + + + Commands + + + git-crypt is logically divided into several sub-commands which + perform distinct tasks. Each sub-command, and its arguments, + are documented below. Note that arguments and options to sub-commands must be + specified on the command line after the name of the sub-command. + + + + + + + + Generate a key and prepare the current Git repository to use git-crypt. + + + + The following options are understood: + + + + KEY_NAME + KEY_NAME + + + + Initialize the given key instead of the default key. git-crypt + supports multiple keys per repository, allowing you to share + different files with different sets of collaborators. + + + + + + + + + + + + Display a list of files in the repository, with their status (encrypted or unencrypted). + + + + The following options are understood: + + + + + + + + Show only encrypted files. + + + + + + + + + Show only unencrypted files. + + + + + + + + + + Encrypt files that should be encrypted but were + committed to the repository or added to the index + without encryption. (This can happen if a file + is added before git-crypt is initialized or before + the file is added to the gitattributes file.) + + + + + + + + + + + + Add the users with the given GPG user IDs as collaborators. Specifically, + git-crypt uses gpg1 + to encrypt the shared symmetric key + to the public keys of each GPG user ID, and stores the GPG-encrypted + keys in the .git-crypt directory at the root of the repository. + + + + GPG_USER_ID can be a key ID, a full fingerprint, an email address, or anything + else that uniquely identifies a public key to GPG (see "HOW TO SPECIFY + A USER ID" in the gpg1 + man page). + + + + The following options are understood: + + + + KEY_NAME + KEY_NAME + + + + Grant access to the given key, rather than the default key. + + + + + + + + + + Don't automatically commit the changes to the .git-crypt + directory. + + + + + + + + + Assume that the GPG keys specified on the command line are trusted; + i.e. they actually belong to the users that they claim to belong to. + + + Without this option, git-crypt uses the same trust model as GPG, + which is based on the Web of Trust by default. Under this + model, git-crypt will reject GPG keys that do not have + trusted signatures. + + + If you don't want to use the Web of Trust, you can either change + GPG's trust model by setting the + option in ~/.gnupg/gpg.conf (see + gpg1), + or use the option to add-gpg-user + on a case-by-case basis. + + + + + + + + + + + + Decrypt the repository. If one or more key files are specified on the command line, + git-crypt attempts to decrypt using those shared symmetric keys. If no key files + are specified, git-crypt attempts to decrypt using a GPG-encrypted key stored in + the repository's .git-crypt directory. + + + + This command takes no options. + + + + + + + + + Export the repository's shared symmetric key to the given file. + + + + The following options are understood: + + + + KEY_NAME + KEY_NAME + + + + Export the given key, rather than the default key. + + + + + + + + + + + + Display help for the given COMMAND, + or an overview of all commands if no command is specified. + + + + + + + + + Print the currently-installed version of git-crypt. + The format of the output is always "git-crypt", followed by a space, + followed by the dotted version number. + + + + + + + + + Using git-crypt + + + First, you prepare a repository to use git-crypt by running git-crypt init. + + + + Then, you specify the files to encrypt by creating a + gitattributes5 file. + Each file which you want to encrypt should be assigned the "filter=git-crypt diff=git-crypt" + attributes. For example: + + + secretfile filter=git-crypt diff=git-crypt *.key filter=git-crypt diff=git-crypt + + + Like a .gitignore file, .gitattributes files can match wildcards and + should be checked into the repository. Make sure you don't accidentally encrypt the + .gitattributes file itself (or other git files like .gitignore + or .gitmodules). Make sure your .gitattributes rules + are in place before you add sensitive files, or those files won't be encrypted! + + + + To share the repository with others (or with yourself) using GPG, run: + + + git-crypt add-gpg-user GPG_USER_ID + + + GPG_USER_ID can be a key ID, a full fingerprint, an email address, or anything + else that uniquely identifies a public key to GPG. Note: git-crypt add-gpg-user will + add and commit a GPG-encrypted key file in the .git-crypt directory of + the root of your repository. + + + + Alternatively, you can export a symmetric secret key, which you must + securely convey to collaborators (GPG is not required, and no files + are added to your repository): + + + git-crypt export-key /path/to/key + + + After cloning a repository with encrypted files, unlock with with GPG: + + + git-crypt unlock + + + Or with a symmetric key: + + + git-crypt unlock /path/to/key + + + That's all you need to do - after git-crypt is set up (either with + git-crypt init or git-crypt unlock), + you can use git normally - encryption and decryption happen transparently. + + + + + + The .gitattributes file + + + The .gitattributes file is documented in + gitattributes5. + The file pattern format is the same as the one used by .gitignore, + as documented in gitignore5, + with the exception that specifying merely a directory (e.g. "/dir/") + is not sufficient to encrypt all files beneath it. + + + + Also note that the pattern "dir/*" does not match files under + sub-directories of dir/. To encrypt an entire sub-tree dir/, place the + following in dir/.gitattributes: + + + * filter=git-crypt diff=git-crypt .gitattributes !filter !diff + + + The second pattern is essential for ensuring that .gitattributes itself + is not encrypted. + + + + + Multiple Key Support + + + In addition to the implicit default key, git-crypt supports alternative + keys which can be used to encrypt specific files and can be shared with + specific GPG users. This is useful if you want to grant different + collaborators access to different sets of files. + + + + To generate an alternative key named KEYNAME, + pass the -k KEYNAME + option to git-crypt init as follows: + + + git-crypt init -k KEYNAME + + + To encrypt a file with an alternative key, use the git-crypt-KEYNAME + filter in .gitattributes as follows: + + + secretfile filter=git-crypt-KEYNAME diff=git-crypt-KEYNAME + + + To export an alternative key or share it with a GPG user, pass the + -k KEYNAME option to + git-crypt export-key or git-crypt add-gpg-user + as follows: + + + git-crypt export-key -k KEYNAME /path/to/keyfile git-crypt add-gpg-user -k KEYNAME GPG_USER_ID + + + To unlock a repository with an alternative key, use git-crypt unlock + normally. git-crypt will automatically determine which key is being used. + + + + + + + + + + + + See Also + + git1, + gitattributes5, + git-crypt home page, + GitHub repository + + + + diff --git a/man/man1/.gitignore b/man/man1/.gitignore new file mode 100644 index 0000000..9f42105 --- /dev/null +++ b/man/man1/.gitignore @@ -0,0 +1 @@ +git-crypt.1 diff --git a/parse_options.cpp b/parse_options.cpp index 51b51f7..008e29d 100644 --- a/parse_options.cpp +++ b/parse_options.cpp @@ -1,31 +1,28 @@ /* * Copyright 2014 Andrew Ayer * - * This file is part of git-crypt. + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: * - * git-crypt 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. + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. * - * git-crypt 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. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. * - * You should have received a copy of the GNU General Public License - * along with git-crypt. If not, see . - * - * Additional permission under GNU GPL version 3 section 7: - * - * If you modify the Program, or any covered work, by linking or - * combining it with the OpenSSL project's OpenSSL library (or a - * modified version of that library), containing parts covered by the - * terms of the OpenSSL or SSLeay licenses, the licensors of the Program - * grant you additional permission to convey the resulting work. - * Corresponding Source for a non-source form of such a combination - * shall include the source code for the parts of OpenSSL used as well - * as that of the covered work. + * Except as contained in this notice, the name(s) of the above copyright + * holders shall not be used in advertising or otherwise to promote the + * sale, use or other dealings in this Software without prior written + * authorization. */ #include "parse_options.hpp" diff --git a/parse_options.hpp b/parse_options.hpp index c0580f0..fa1ef87 100644 --- a/parse_options.hpp +++ b/parse_options.hpp @@ -1,31 +1,28 @@ /* * Copyright 2014 Andrew Ayer * - * This file is part of git-crypt. + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: * - * git-crypt 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. + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. * - * git-crypt 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. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. * - * You should have received a copy of the GNU General Public License - * along with git-crypt. If not, see . - * - * Additional permission under GNU GPL version 3 section 7: - * - * If you modify the Program, or any covered work, by linking or - * combining it with the OpenSSL project's OpenSSL library (or a - * modified version of that library), containing parts covered by the - * terms of the OpenSSL or SSLeay licenses, the licensors of the Program - * grant you additional permission to convey the resulting work. - * Corresponding Source for a non-source form of such a combination - * shall include the source code for the parts of OpenSSL used as well - * as that of the covered work. + * Except as contained in this notice, the name(s) of the above copyright + * holders shall not be used in advertising or otherwise to promote the + * sale, use or other dealings in this Software without prior written + * authorization. */ #ifndef PARSE_OPTIONS_HPP diff --git a/util-unix.cpp b/util-unix.cpp index 1cebf3f..7c7c05b 100644 --- a/util-unix.cpp +++ b/util-unix.cpp @@ -43,6 +43,8 @@ #include #include #include +#include +#include std::string System_error::message () const { @@ -160,134 +162,21 @@ std::string our_exe_path () } } -static int execvp (const std::string& file, const std::vector& args) +int exit_status (int wait_status) { - std::vector args_c_str; - args_c_str.reserve(args.size()); - for (std::vector::const_iterator arg(args.begin()); arg != args.end(); ++arg) { - args_c_str.push_back(arg->c_str()); - } - args_c_str.push_back(NULL); - return execvp(file.c_str(), const_cast(&args_c_str[0])); -} - -int exec_command (const std::vector& command) -{ - pid_t child = fork(); - if (child == -1) { - throw System_error("fork", "", errno); - } - if (child == 0) { - execvp(command[0], command); - perror(command[0].c_str()); - _exit(-1); - } - int status = 0; - if (waitpid(child, &status, 0) == -1) { - throw System_error("waitpid", "", errno); - } - return status; -} - -int exec_command (const std::vector& command, std::ostream& output) -{ - int pipefd[2]; - if (pipe(pipefd) == -1) { - throw System_error("pipe", "", errno); - } - pid_t child = fork(); - if (child == -1) { - int fork_errno = errno; - close(pipefd[0]); - close(pipefd[1]); - throw System_error("fork", "", fork_errno); - } - if (child == 0) { - close(pipefd[0]); - if (pipefd[1] != 1) { - dup2(pipefd[1], 1); - close(pipefd[1]); - } - execvp(command[0], command); - perror(command[0].c_str()); - _exit(-1); - } - close(pipefd[1]); - char buffer[1024]; - ssize_t bytes_read; - while ((bytes_read = read(pipefd[0], buffer, sizeof(buffer))) > 0) { - output.write(buffer, bytes_read); - } - if (bytes_read == -1) { - int read_errno = errno; - close(pipefd[0]); - throw System_error("read", "", read_errno); - } - close(pipefd[0]); - int status = 0; - if (waitpid(child, &status, 0) == -1) { - throw System_error("waitpid", "", errno); - } - return status; -} - -int exec_command_with_input (const std::vector& command, const char* p, size_t len) -{ - int pipefd[2]; - if (pipe(pipefd) == -1) { - throw System_error("pipe", "", errno); - } - pid_t child = fork(); - if (child == -1) { - int fork_errno = errno; - close(pipefd[0]); - close(pipefd[1]); - throw System_error("fork", "", fork_errno); - } - if (child == 0) { - close(pipefd[1]); - if (pipefd[0] != 0) { - dup2(pipefd[0], 0); - close(pipefd[0]); - } - execvp(command[0], command); - perror(command[0].c_str()); - _exit(-1); - } - close(pipefd[0]); - while (len > 0) { - ssize_t bytes_written = write(pipefd[1], p, len); - if (bytes_written == -1) { - int write_errno = errno; - close(pipefd[1]); - throw System_error("write", "", write_errno); - } - p += bytes_written; - len -= bytes_written; - } - close(pipefd[1]); - int status = 0; - if (waitpid(child, &status, 0) == -1) { - throw System_error("waitpid", "", errno); - } - return status; -} - -bool successful_exit (int status) -{ - return status != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0; + return wait_status != -1 && WIFEXITED(wait_status) ? WEXITSTATUS(wait_status) : -1; } void touch_file (const std::string& filename) { - if (utimes(filename.c_str(), NULL) == -1) { + if (utimes(filename.c_str(), NULL) == -1 && errno != ENOENT) { throw System_error("utimes", filename, errno); } } void remove_file (const std::string& filename) { - if (unlink(filename.c_str()) == -1) { + if (unlink(filename.c_str()) == -1 && errno != ENOENT) { throw System_error("unlink", filename, errno); } } @@ -310,25 +199,47 @@ int util_rename (const char* from, const char* to) return rename(from, to); } -static int dirfilter (const struct dirent* ent) +static size_t sizeof_dirent_for (DIR* p) { - // filter out . and .. - return std::strcmp(ent->d_name, ".") != 0 && std::strcmp(ent->d_name, "..") != 0; + long name_max = fpathconf(dirfd(p), _PC_NAME_MAX); + if (name_max == -1) { + #ifdef NAME_MAX + name_max = NAME_MAX; + #else + name_max = 255; + #endif + } + return offsetof(struct dirent, d_name) + name_max + 1; // final +1 is for d_name's null terminator } std::vector get_directory_contents (const char* path) { - struct dirent** namelist; - int n = scandir(path, &namelist, dirfilter, alphasort); - if (n == -1) { - throw System_error("scandir", path, errno); - } - std::vector contents(n); - for (int i = 0; i < n; ++i) { - contents[i] = namelist[i]->d_name; - free(namelist[i]); - } - free(namelist); + std::vector contents; + DIR* dir = opendir(path); + if (!dir) { + throw System_error("opendir", path, errno); + } + try { + std::vector buffer(sizeof_dirent_for(dir)); + struct dirent* dirent_buffer = reinterpret_cast(&buffer[0]); + struct dirent* ent = NULL; + int err = 0; + while ((err = readdir_r(dir, dirent_buffer, &ent)) == 0 && ent != NULL) { + if (std::strcmp(ent->d_name, ".") == 0 || std::strcmp(ent->d_name, "..") == 0) { + continue; + } + contents.push_back(ent->d_name); + } + if (err != 0) { + throw System_error("readdir_r", path, errno); + } + } catch (...) { + closedir(dir); + throw; + } + closedir(dir); + + std::sort(contents.begin(), contents.end()); return contents; } diff --git a/util-win32.cpp b/util-win32.cpp index 21576c7..445d185 100644 --- a/util-win32.cpp +++ b/util-win32.cpp @@ -125,207 +125,21 @@ std::string our_exe_path () return std::string(buffer.begin(), buffer.begin() + len); } -static void escape_cmdline_argument (std::string& cmdline, const std::string& arg) +int exit_status (int status) { - // For an explanation of Win32's arcane argument quoting rules, see: - // http://msdn.microsoft.com/en-us/library/17w5ykft%28v=vs.85%29.aspx - // http://msdn.microsoft.com/en-us/library/bb776391%28v=vs.85%29.aspx - // http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx - // http://blogs.msdn.com/b/oldnewthing/archive/2010/09/17/10063629.aspx - cmdline.push_back('"'); - - std::string::const_iterator p(arg.begin()); - while (p != arg.end()) { - if (*p == '"') { - cmdline.push_back('\\'); - cmdline.push_back('"'); - ++p; - } else if (*p == '\\') { - unsigned int num_backslashes = 0; - while (p != arg.end() && *p == '\\') { - ++num_backslashes; - ++p; - } - if (p == arg.end() || *p == '"') { - // Backslashes need to be escaped - num_backslashes *= 2; - } - while (num_backslashes--) { - cmdline.push_back('\\'); - } - } else { - cmdline.push_back(*p++); - } - } - - cmdline.push_back('"'); -} - -static std::string format_cmdline (const std::vector& command) -{ - std::string cmdline; - for (std::vector::const_iterator arg(command.begin()); arg != command.end(); ++arg) { - if (arg != command.begin()) { - cmdline.push_back(' '); - } - escape_cmdline_argument(cmdline, *arg); - } - return cmdline; -} - -static int wait_for_child (HANDLE child_handle) -{ - if (WaitForSingleObject(child_handle, INFINITE) == WAIT_FAILED) { - throw System_error("WaitForSingleObject", "", GetLastError()); - } - - DWORD exit_code; - if (!GetExitCodeProcess(child_handle, &exit_code)) { - throw System_error("GetExitCodeProcess", "", GetLastError()); - } - - return exit_code; -} - -static HANDLE spawn_command (const std::vector& command, HANDLE stdin_handle, HANDLE stdout_handle, HANDLE stderr_handle) -{ - PROCESS_INFORMATION proc_info; - ZeroMemory(&proc_info, sizeof(proc_info)); - - STARTUPINFO start_info; - ZeroMemory(&start_info, sizeof(start_info)); - - start_info.cb = sizeof(STARTUPINFO); - start_info.hStdInput = stdin_handle ? stdin_handle : GetStdHandle(STD_INPUT_HANDLE); - start_info.hStdOutput = stdout_handle ? stdout_handle : GetStdHandle(STD_OUTPUT_HANDLE); - start_info.hStdError = stderr_handle ? stderr_handle : GetStdHandle(STD_ERROR_HANDLE); - start_info.dwFlags |= STARTF_USESTDHANDLES; - - std::string cmdline(format_cmdline(command)); - - if (!CreateProcessA(NULL, // application name (NULL to use command line) - const_cast(cmdline.c_str()), - NULL, // process security attributes - NULL, // primary thread security attributes - TRUE, // handles are inherited - 0, // creation flags - NULL, // use parent's environment - NULL, // use parent's current directory - &start_info, - &proc_info)) { - throw System_error("CreateProcess", cmdline, GetLastError()); - } - - CloseHandle(proc_info.hThread); - - return proc_info.hProcess; -} - -int exec_command (const std::vector& command) -{ - HANDLE child_handle = spawn_command(command, NULL, NULL, NULL); - int exit_code = wait_for_child(child_handle); - CloseHandle(child_handle); - return exit_code; -} - -int exec_command (const std::vector& command, std::ostream& output) -{ - HANDLE stdout_pipe_reader = NULL; - HANDLE stdout_pipe_writer = NULL; - SECURITY_ATTRIBUTES sec_attr; - - // Set the bInheritHandle flag so pipe handles are inherited. - sec_attr.nLength = sizeof(SECURITY_ATTRIBUTES); - sec_attr.bInheritHandle = TRUE; - sec_attr.lpSecurityDescriptor = NULL; - - // Create a pipe for the child process's STDOUT. - if (!CreatePipe(&stdout_pipe_reader, &stdout_pipe_writer, &sec_attr, 0)) { - throw System_error("CreatePipe", "", GetLastError()); - } - - // Ensure the read handle to the pipe for STDOUT is not inherited. - if (!SetHandleInformation(stdout_pipe_reader, HANDLE_FLAG_INHERIT, 0)) { - throw System_error("SetHandleInformation", "", GetLastError()); - } - - HANDLE child_handle = spawn_command(command, NULL, stdout_pipe_writer, NULL); - CloseHandle(stdout_pipe_writer); - - // Read from stdout_pipe_reader. - // Note that ReadFile on a pipe may return with bytes_read==0 if the other - // end of the pipe writes zero bytes, so don't break out of the read loop - // when this happens. When the other end of the pipe closes, ReadFile - // fails with ERROR_BROKEN_PIPE. - char buffer[1024]; - DWORD bytes_read; - while (ReadFile(stdout_pipe_reader, buffer, sizeof(buffer), &bytes_read, NULL)) { - output.write(buffer, bytes_read); - } - const DWORD read_error = GetLastError(); - if (read_error != ERROR_BROKEN_PIPE) { - throw System_error("ReadFile", "", read_error); - } - - CloseHandle(stdout_pipe_reader); - - int exit_code = wait_for_child(child_handle); - CloseHandle(child_handle); - return exit_code; -} - -int exec_command_with_input (const std::vector& command, const char* p, size_t len) -{ - HANDLE stdin_pipe_reader = NULL; - HANDLE stdin_pipe_writer = NULL; - SECURITY_ATTRIBUTES sec_attr; - - // Set the bInheritHandle flag so pipe handles are inherited. - sec_attr.nLength = sizeof(SECURITY_ATTRIBUTES); - sec_attr.bInheritHandle = TRUE; - sec_attr.lpSecurityDescriptor = NULL; - - // Create a pipe for the child process's STDIN. - if (!CreatePipe(&stdin_pipe_reader, &stdin_pipe_writer, &sec_attr, 0)) { - throw System_error("CreatePipe", "", GetLastError()); - } - - // Ensure the write handle to the pipe for STDIN is not inherited. - if (!SetHandleInformation(stdin_pipe_writer, HANDLE_FLAG_INHERIT, 0)) { - throw System_error("SetHandleInformation", "", GetLastError()); - } - - HANDLE child_handle = spawn_command(command, stdin_pipe_reader, NULL, NULL); - CloseHandle(stdin_pipe_reader); - - // Write to stdin_pipe_writer. - while (len > 0) { - DWORD bytes_written; - if (!WriteFile(stdin_pipe_writer, p, len, &bytes_written, NULL)) { - throw System_error("WriteFile", "", GetLastError()); - } - p += bytes_written; - len -= bytes_written; - } - - CloseHandle(stdin_pipe_writer); - - int exit_code = wait_for_child(child_handle); - CloseHandle(child_handle); - return exit_code; -} - -bool successful_exit (int status) -{ - return status == 0; + return status; } void touch_file (const std::string& filename) { HANDLE fh = CreateFileA(filename.c_str(), FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (fh == INVALID_HANDLE_VALUE) { - throw System_error("CreateFileA", filename, GetLastError()); + DWORD error = GetLastError(); + if (error == ERROR_FILE_NOT_FOUND) { + return; + } else { + throw System_error("CreateFileA", filename, error); + } } SYSTEMTIME system_time; GetSystemTime(&system_time); @@ -343,7 +157,12 @@ void touch_file (const std::string& filename) void remove_file (const std::string& filename) { if (!DeleteFileA(filename.c_str())) { - throw System_error("DeleteFileA", filename, GetLastError()); + DWORD error = GetLastError(); + if (error == ERROR_FILE_NOT_FOUND) { + return; + } else { + throw System_error("DeleteFileA", filename, error); + } } } diff --git a/util.cpp b/util.cpp index 2da0622..4e1aa78 100644 --- a/util.cpp +++ b/util.cpp @@ -30,9 +30,36 @@ #include "git-crypt.hpp" #include "util.hpp" +#include "coprocess.hpp" #include #include +int exec_command (const std::vector& args) +{ + Coprocess proc; + proc.spawn(args); + return proc.wait(); +} + +int exec_command (const std::vector& args, std::ostream& output) +{ + Coprocess proc; + std::istream* proc_stdout = proc.stdout_pipe(); + proc.spawn(args); + output << proc_stdout->rdbuf(); + return proc.wait(); +} + +int exec_command_with_input (const std::vector& args, const char* p, size_t len) +{ + Coprocess proc; + std::ostream* proc_stdin = proc.stdin_pipe(); + proc.spawn(args); + proc_stdin->write(p, len); + proc.close_stdin(); + return proc.wait(); +} + std::string escape_shell_arg (const std::string& str) { std::string new_str; diff --git a/util.hpp b/util.hpp index 8b5bc33..2a9d77a 100644 --- a/util.hpp +++ b/util.hpp @@ -63,9 +63,10 @@ std::string our_exe_path (); int exec_command (const std::vector&); int exec_command (const std::vector&, std::ostream& output); int exec_command_with_input (const std::vector&, const char* p, size_t len); -bool successful_exit (int status); -void touch_file (const std::string&); -void remove_file (const std::string&); +int exit_status (int wait_status); // returns -1 if process did not exit (but was signaled, etc.) +inline bool successful_exit (int wait_status) { return exit_status(wait_status) == 0; } +void touch_file (const std::string&); // ignores non-existent files +void remove_file (const std::string&); // ignores non-existent files std::string escape_shell_arg (const std::string&); uint32_t load_be32 (const unsigned char*); void store_be32 (unsigned char*, uint32_t);