From 7687d112190c65cb180d53eb52d46a2f6b184f83 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Fri, 28 Mar 2014 14:02:25 -0700 Subject: [PATCH] Initial GPG support Run 'git-crypt add-collab KEYID' to authorize the holder of the given GPG secret key to access the encrypted files. The secret git-crypt key will be encrypted with the corresponding GPG public key and stored in the root of the Git repository under .git-crypt/keys. After cloning a repo with encrypted files, run 'git-crypt unlock' (with no arguments) to use a secret key in your GPG keyring to unlock the repository. Multiple collaborators are supported, however commands to list the collaborators ('git-crypt ls-collabs') and to remove a collaborator ('git-crypt rm-collab') are not yet supported. --- Makefile | 2 +- commands.cpp | 164 ++++++++++++++++++++++++++++++++++++++++++++++---- git-crypt.cpp | 4 ++ gpg.cpp | 154 +++++++++++++++++++++++++++++++++++++++++++++++ gpg.hpp | 51 ++++++++++++++++ key.cpp | 8 +++ key.hpp | 3 + util.cpp | 42 +++++++++++++ util.hpp | 1 + 9 files changed, 415 insertions(+), 14 deletions(-) create mode 100644 gpg.cpp create mode 100644 gpg.hpp diff --git a/Makefile b/Makefile index e3f9920..5b48350 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ CXXFLAGS := -Wall -pedantic -ansi -Wno-long-long -O2 LDFLAGS := -lcrypto PREFIX := /usr/local -OBJFILES = git-crypt.o commands.o crypto.o key.o util.o +OBJFILES = git-crypt.o commands.o crypto.o gpg.o key.o util.o all: git-crypt diff --git a/commands.cpp b/commands.cpp index f7b60f3..7c859a7 100644 --- a/commands.cpp +++ b/commands.cpp @@ -32,6 +32,7 @@ #include "crypto.hpp" #include "util.hpp" #include "key.hpp" +#include "gpg.hpp" #include #include #include @@ -46,6 +47,7 @@ #include #include #include +#include static void configure_git_filters () { @@ -90,6 +92,26 @@ static std::string get_internal_key_path () return path; } +static std::string get_repo_keys_path () +{ + std::stringstream output; + + if (!successful_exit(exec_command("git rev-parse --show-toplevel", output))) { + throw Error("'git rev-parse --show-toplevel' failed - is this a Git repository?"); + } + + std::string path; + std::getline(output, path); + + if (path.empty()) { + // could happen for a bare repo + throw Error("Could not determine Git working tree - is this a non-bare repo?"); + } + + path += "/.git-crypt/keys"; + return path; +} + static void load_key (Key_file& key_file, const char* legacy_path =0) { if (legacy_path) { @@ -107,6 +129,53 @@ static void load_key (Key_file& key_file, const char* legacy_path =0) } } +static bool decrypt_repo_key (Key_file& key_file, uint32_t key_version, const std::vector& secret_keys, const std::string& keys_path) +{ + for (std::vector::const_iterator seckey(secret_keys.begin()); seckey != secret_keys.end(); ++seckey) { + std::ostringstream path_builder; + path_builder << keys_path << '/' << key_version << '/' << *seckey; + std::string path(path_builder.str()); + if (access(path.c_str(), F_OK) == 0) { + std::stringstream decrypted_contents; + gpg_decrypt_from_file(path, decrypted_contents); + Key_file this_version_key_file; + this_version_key_file.load(decrypted_contents); + const Key_file::Entry* this_version_entry = this_version_key_file.get(key_version); + if (!this_version_entry) { + throw Error("GPG-encrypted keyfile is malformed because it does not contain expected key version"); + } + key_file.add(key_version, *this_version_entry); + return true; + } + } + return false; +} + +static void encrypt_repo_key (uint32_t key_version, const Key_file::Entry& key, const std::vector& collab_keys, const std::string& keys_path, std::vector* new_files) +{ + std::string key_file_data; + { + Key_file this_version_key_file; + this_version_key_file.add(key_version, 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) { + std::ostringstream path_builder; + path_builder << keys_path << '/' << key_version << '/' << *collab; + std::string path(path_builder.str()); + + if (access(path.c_str(), F_OK) == 0) { + continue; + } + + mkdir_parent(path); + gpg_encrypt_to_file(path, *collab, key_file_data.data(), key_file_data.size()); + new_files->push_back(path); + } +} + + // Encrypt contents of stdin and write to stdout int clean (int argc, char** argv) @@ -409,9 +478,17 @@ int unlock (int argc, char** argv) return 1; } } else { - // Decrypt GPG key from root of repo (TODO NOW) - std::clog << "Error: GPG support is not yet implemented" << std::endl; - return 1; + // Decrypt GPG key from root of repo + std::string repo_keys_path(get_repo_keys_path()); + std::vector gpg_secret_keys(gpg_list_secret_keys()); + // TODO: command-line option to specify the precise secret key to use + // TODO: don't hard code key version 0 here - instead, determine the most recent version and try to decrypt that, or decrypt all versions if command-line option specified + if (!decrypt_repo_key(key_file, 0, gpg_secret_keys, repo_keys_path)) { + std::clog << "Error: no GPG secret key available to unlock this repository." << std::endl; + std::clog << "To unlock with a shared symmetric key instead, specify the path to the symmetric key as an argument to 'git-crypt unlock'." << std::endl; + std::clog << "To see a list of GPG keys authorized to unlock this repository, run 'git-crypt ls-collabs'." << std::endl; + return 1; + } } std::string internal_key_path(get_internal_key_path()); // TODO: croak if internal_key_path already exists??? @@ -449,17 +526,78 @@ int unlock (int argc, char** argv) return 0; } -int add_collab (int argc, char** argv) // TODO NOW +int add_collab (int argc, char** argv) { - // Sketch: - // 1. Resolve the key ID to a long hex ID - // 2. Create the in-repo key directory if it doesn't exist yet. - // 3. For most recent key version KEY_VERSION (or for each key version KEY_VERSION if retroactive option specified): - // Encrypt KEY_VERSION with the GPG key and stash it in .git-crypt/keys/KEY_VERSION/LONG_HEX_ID - // if file already exists, print a notice and move on - // 4. Commit the new file(s) (if any) with a meanignful commit message, unless -n was passed - std::clog << "Error: add-collab is not yet implemented." << std::endl; - return 1; + if (argc == 0) { + std::clog << "Usage: git-crypt add-collab GPG_USER_ID [...]" << std::endl; + return 2; + } + + // build a list of key fingerprints for every collaborator specified on the command line + std::vector collab_keys; + + for (int i = 0; i < argc; ++i) { + std::vector keys(gpg_lookup_key(argv[i])); + if (keys.empty()) { + std::clog << "Error: public key for '" << argv[i] << "' not found in your GPG keyring" << std::endl; + return 1; + } + if (keys.size() > 1) { + 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]); + } + + // TODO: have a retroactive option to grant access to all key versions, not just the most recent + Key_file key_file; + load_key(key_file); + const Key_file::Entry* key = key_file.get_latest(); + if (!key) { + std::clog << "Error: key file is empty" << std::endl; + return 1; + } + + std::string keys_path(get_repo_keys_path()); + std::vector new_files; + + encrypt_repo_key(key_file.latest(), *key, collab_keys, keys_path, &new_files); + + // add/commit the new files + if (!new_files.empty()) { + // git add ... + std::string command("git add"); + for (std::vector::const_iterator file(new_files.begin()); file != new_files.end(); ++file) { + command += " "; + command += escape_shell_arg(*file); + } + if (!successful_exit(system(command.c_str()))) { + std::clog << "Error: 'git add' failed" << std::endl; + return 1; + } + + // git commit ... + // TODO: add a command line option (-n perhaps) to inhibit committing + 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'; + } + + command = "git commit -m "; + command += escape_shell_arg(commit_message_builder.str()); + for (std::vector::const_iterator file(new_files.begin()); file != new_files.end(); ++file) { + command += " "; + command += escape_shell_arg(*file); + } + + if (!successful_exit(system(command.c_str()))) { + std::clog << "Error: 'git commit' failed" << std::endl; + return 1; + } + } + + return 0; } int rm_collab (int argc, char** argv) // TODO diff --git a/git-crypt.cpp b/git-crypt.cpp index 69d7fed..8bed1a0 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -33,6 +33,7 @@ #include "util.hpp" #include "crypto.hpp" #include "key.hpp" +#include "gpg.hpp" #include #include #include @@ -181,6 +182,9 @@ try { } catch (const Error& e) { std::cerr << "git-crypt: Error: " << e.message << std::endl; return 1; +} catch (const Gpg_error& e) { + std::cerr << "git-crypt: GPG error: " << e.message << std::endl; + return 1; } catch (const System_error& e) { std::cerr << "git-crypt: " << e.action << ": "; if (!e.target.empty()) { diff --git a/gpg.cpp b/gpg.cpp new file mode 100644 index 0000000..05db9fb --- /dev/null +++ b/gpg.cpp @@ -0,0 +1,154 @@ +/* + * Copyright 2014 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 "gpg.hpp" +#include "util.hpp" +#include + +static std::string gpg_nth_column (const std::string& line, unsigned int col) +{ + std::string::size_type pos = 0; + + for (unsigned int i = 0; i < col; ++i) { + pos = line.find_first_of(':', pos); + if (pos == std::string::npos) { + throw Gpg_error("Malformed output from gpg"); + } + pos = pos + 1; + } + + const std::string::size_type end_pos = line.find_first_of(':', pos); + + return end_pos != std::string::npos ? + line.substr(pos, end_pos - pos) : + line.substr(pos); +} + +// given a key fingerprint, return the last 8 nibbles +std::string gpg_shorten_fingerprint (const std::string& fingerprint) +{ + return fingerprint.size() == 40 ? fingerprint.substr(32) : fingerprint; +} + +// given a key fingerprint, return the key's UID (e.g. "John Smith ") +std::string gpg_get_uid (const std::string& fingerprint) +{ + // gpg --batch --with-colons --fixed-list-mode --list-keys 0x7A399B2DB06D039020CD1CE1D0F3702D61489532 + std::string command("gpg --batch --with-colons --fixed-list-mode --list-keys "); + command += escape_shell_arg("0x" + fingerprint); + std::stringstream command_output; + if (!successful_exit(exec_command(command.c_str(), command_output))) { + // This could happen if the keyring does not contain a public key with this fingerprint + return ""; + } + + while (command_output.peek() != -1) { + std::string line; + std::getline(command_output, line); + if (line.substr(0, 4) == "uid:") { + // uid:u::::1395975462::AB97D6E3E5D8789988CA55E5F77D9E7397D05229::John Smith : + // want the 9th column (counting from 0) + return gpg_nth_column(line, 9); + } + } + + return ""; +} + +// return a list of fingerprints of public keys matching the given search query (such as jsmith@example.com) +std::vector gpg_lookup_key (const std::string& query) +{ + std::vector fingerprints; + + // gpg --batch --with-colons --fingerprint --list-keys jsmith@example.com + std::string command("gpg --batch --with-colons --fingerprint --list-keys "); + command += escape_shell_arg(query); + std::stringstream command_output; + if (successful_exit(exec_command(command.c_str(), command_output))) { + while (command_output.peek() != -1) { + std::string line; + std::getline(command_output, line); + if (line.substr(0, 4) == "fpr:") { + // fpr:::::::::7A399B2DB06D039020CD1CE1D0F3702D61489532: + // want the 9th column (counting from 0) + fingerprints.push_back(gpg_nth_column(line, 9)); + } + } + } + + return fingerprints; +} + +std::vector gpg_list_secret_keys () +{ + // gpg --batch --with-colons --list-secret-keys --fingerprint + std::stringstream command_output; + if (!successful_exit(exec_command("gpg --batch --with-colons --list-secret-keys --fingerprint", command_output))) { + throw Gpg_error("gpg --list-secret-keys failed"); + } + + std::vector secret_keys; + + while (command_output.peek() != -1) { + std::string line; + std::getline(command_output, line); + if (line.substr(0, 4) == "fpr:") { + // fpr:::::::::7A399B2DB06D039020CD1CE1D0F3702D61489532: + // want the 9th column (counting from 0) + secret_keys.push_back(gpg_nth_column(line, 9)); + } + } + + return secret_keys; +} + +void gpg_encrypt_to_file (const std::string& filename, const std::string& recipient_fingerprint, const char* p, size_t len) +{ + // gpg --batch -o FILENAME -r RECIPIENT -e + std::string command("gpg --batch -o "); + command += escape_shell_arg(filename); + command += " -r "; + command += escape_shell_arg("0x" + recipient_fingerprint); + command += " -e"; + if (!successful_exit(exec_command_with_input(command.c_str(), p, len))) { + throw Gpg_error("Failed to encrypt"); + } +} + +void gpg_decrypt_from_file (const std::string& filename, std::ostream& output) +{ + // gpg -q -d + std::string command("gpg -q -d "); + command += escape_shell_arg(filename); + if (!successful_exit(exec_command(command.c_str(), output))) { + throw Gpg_error("Failed to decrypt"); + } +} + diff --git a/gpg.hpp b/gpg.hpp new file mode 100644 index 0000000..c2939bb --- /dev/null +++ b/gpg.hpp @@ -0,0 +1,51 @@ +/* + * Copyright 2014 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 _GPG_H +#define _GPG_H + +#include +#include +#include + +struct Gpg_error { + std::string message; + + explicit Gpg_error (std::string m) : message(m) { } +}; + +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_decrypt_from_file (const std::string& filename, std::ostream&); + +#endif diff --git a/key.cpp b/key.cpp index bf39261..2fea653 100644 --- a/key.cpp +++ b/key.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include @@ -146,6 +147,13 @@ bool Key_file::store_to_file (const char* key_file_name) const return true; } +std::string Key_file::store_to_string () const +{ + std::ostringstream ss; + store(ss); + return ss.str(); +} + void Key_file::generate () { entries[is_empty() ? 0 : latest() + 1].generate(); diff --git a/key.hpp b/key.hpp index d16241f..c237d30 100644 --- a/key.hpp +++ b/key.hpp @@ -35,6 +35,7 @@ #include #include #include +#include enum { HMAC_KEY_LEN = 64, @@ -67,6 +68,8 @@ public: bool load_from_file (const char* filename); bool store_to_file (const char* filename) const; + std::string store_to_string () const; + void generate (); bool is_empty () const { return entries.empty(); } diff --git a/util.cpp b/util.cpp index 2afd685..cd1c514 100644 --- a/util.cpp +++ b/util.cpp @@ -146,6 +146,48 @@ int exec_command (const char* command, std::ostream& output) return status; } +int exec_command_with_input (const char* 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]); + } + execl("/bin/sh", "sh", "-c", command, NULL); + perror("/bin/sh"); + _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; diff --git a/util.hpp b/util.hpp index 37fa523..9c45095 100644 --- a/util.hpp +++ b/util.hpp @@ -48,6 +48,7 @@ void mkdir_parent (const std::string& path); // Create parent directories of pa std::string readlink (const char* pathname); std::string our_exe_path (); int exec_command (const char* command, std::ostream& output); +int exec_command_with_input (const char* command, const char* p, size_t len); bool successful_exit (int status); void open_tempfile (std::fstream&, std::ios_base::openmode); std::string escape_shell_arg (const std::string&);