From 2f0216104280231cb02a807af2ec8d3047b0143f Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 22 Mar 2014 11:41:18 -0700 Subject: [PATCH 01/47] Add utility functions for big-endian int storage Use instead of htonl(). --- crypto.cpp | 7 +++---- util.cpp | 34 ++++++++++++++++++++++++++++++++++ util.hpp | 5 +++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/crypto.cpp b/crypto.cpp index e1a8594..25c9ae0 100644 --- a/crypto.cpp +++ b/crypto.cpp @@ -28,8 +28,8 @@ * as that of the covered work. */ -#define _BSD_SOURCE #include "crypto.hpp" +#include "util.hpp" #include #include #include @@ -38,7 +38,6 @@ #include #include #include -#include void load_keys (const char* filepath, keys_t* keys) { @@ -82,9 +81,9 @@ void aes_ctr_state::process (const AES_KEY* key, const uint8_t* in, uint8_t* out // first 12 bytes - nonce // last 4 bytes - block number (sequentially increasing with each block) uint8_t ctr[16]; - uint32_t blockno = htonl(byte_counter / 16); + uint32_t blockno = byte_counter / 16; memcpy(ctr, nonce, 12); - memcpy(ctr + 12, &blockno, 4); + store_be32(ctr + 12, blockno); AES_encrypt(ctr, otp, key); } diff --git a/util.cpp b/util.cpp index 575d616..e37d7cc 100644 --- a/util.cpp +++ b/util.cpp @@ -126,3 +126,37 @@ std::string escape_shell_arg (const std::string& str) return new_str; } +uint32_t load_be32 (const unsigned char* p) +{ + return (static_cast(p[3]) << 0) | + (static_cast(p[2]) << 8) | + (static_cast(p[1]) << 16) | + (static_cast(p[0]) << 24); +} + +void store_be32 (unsigned char* p, uint32_t i) +{ + p[3] = i; i >>= 8; + p[2] = i; i >>= 8; + p[1] = i; i >>= 8; + p[0] = i; +} + +bool read_be32 (std::istream& in, uint32_t& i) +{ + unsigned char buffer[4]; + in.read(reinterpret_cast(buffer), 4); + if (in.gcount() != 4) { + return false; + } + i = load_be32(buffer); + return true; +} + +void write_be32 (std::ostream& out, uint32_t i) +{ + unsigned char buffer[4]; + store_be32(buffer, i); + out.write(reinterpret_cast(buffer), 4); +} + diff --git a/util.hpp b/util.hpp index aa76982..2bd7356 100644 --- a/util.hpp +++ b/util.hpp @@ -34,11 +34,16 @@ #include #include #include +#include int exec_command (const char* command, std::ostream& output); std::string resolve_path (const char* path); void open_tempfile (std::fstream&, std::ios_base::openmode); std::string escape_shell_arg (const std::string&); +uint32_t load_be32 (const unsigned char*); +void store_be32 (unsigned char*, uint32_t); +bool read_be32 (std::istream& in, uint32_t&); +void write_be32 (std::ostream& out, uint32_t); #endif From 6a454b1fa158558080299ca88f033a84b86f9f39 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sun, 23 Mar 2014 11:17:26 -0700 Subject: [PATCH 02/47] Major revamp: new key paradigm, groundwork for GPG support The active key is now stored in .git/git-crypt/key instead of being stored outside the repo. This will facilitate GPG support, where the user may never interact directly with a key file. It's also more convenient, because it means you don't have to keep the key file around in a fixed location (which can't be moved without breaking git-crypt). 'git-crypt init' now takes no arguments and is used only when initializing git-crypt for the very first time. It generates a brand-new key, so there's no longer a separate keygen step. To export the key (for conveyance to another system or to a collaborator), run 'git-crypt export-key FILENAME'. To decrypt an existing repo using an exported key, run 'git-crypt unlock KEYFILE'. After running unlock, you can delete the key file you passed to unlock. Key files now use a new format that supports key versioning (which will facilitate secure revocation in the future). I've made these changes as backwards-compatible as possible. Repos already configured with git-crypt will continue to work without changes. However, 'git-crypt unlock' expects a new format key. You can use the 'git-crypt migrate-key KEYFILE' command to migrate old keys to the new format. Note that old repos won't be able to use the new commands, like export-key, or the future GPG support. To migrate an old repo, migrate its key file and then unlock the repo using the unlock command, as described above. While making these changes, I cleaned up the code significantly, adding better error handling and improving robustness. Next up: GPG support. --- Makefile | 2 +- commands.cpp | 546 ++++++++++++++++++++++++++++++++++++++------------ commands.hpp | 28 ++- crypto.cpp | 96 +++++---- crypto.hpp | 70 ++++--- git-crypt.cpp | 178 +++++++++++++--- git-crypt.hpp | 36 ++++ key.cpp | 161 +++++++++++++++ key.hpp | 84 ++++++++ util.cpp | 121 ++++++++--- util.hpp | 15 +- 11 files changed, 1066 insertions(+), 271 deletions(-) create mode 100644 git-crypt.hpp create mode 100644 key.cpp create mode 100644 key.hpp diff --git a/Makefile b/Makefile index f0b8e60..e3f9920 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 util.o +OBJFILES = git-crypt.o commands.o crypto.o key.o util.o all: git-crypt diff --git a/commands.cpp b/commands.cpp index b3180c5..a0fc292 100644 --- a/commands.cpp +++ b/commands.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -31,6 +31,7 @@ #include "commands.hpp" #include "crypto.hpp" #include "util.hpp" +#include "key.hpp" #include #include #include @@ -42,26 +43,102 @@ #include #include #include -#include -#include +#include +#include +#include + +static void configure_git_filters () +{ + std::string git_crypt_path(our_exe_path()); + + // git config filter.git-crypt.smudge "/path/to/git-crypt smudge" + std::string command("git config filter.git-crypt.smudge "); + command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " smudge"); + + if (system(command.c_str()) != 0) { + throw Error("'git config' failed"); + } + + // git config filter.git-crypt.clean "/path/to/git-crypt clean" + command = "git config filter.git-crypt.clean "; + command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " clean"); + + if (system(command.c_str()) != 0) { + throw Error("'git config' failed"); + } + + // git config diff.git-crypt.textconv "/path/to/git-crypt diff" + command = "git config diff.git-crypt.textconv "; + command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " diff"); + + if (system(command.c_str()) != 0) { + throw Error("'git config' failed"); + } +} + +static std::string get_internal_key_path () +{ + std::stringstream output; + + if (!successful_exit(exec_command("git rev-parse --git-dir", output))) { + throw Error("'git rev-parse --git-dir' - is this a Git repository?"); + } + + std::string path; + std::getline(output, path); + path += "/git-crypt/key"; + return path; +} + +static void load_key (Key_file& key_file, const char* legacy_path =0) +{ + if (legacy_path) { + std::ifstream key_file_in(legacy_path, std::fstream::binary); + if (!key_file_in) { + throw Error(std::string("Unable to open key file: ") + legacy_path); + } + key_file.load_legacy(key_file_in); + } else { + std::ifstream key_file_in(get_internal_key_path().c_str(), std::fstream::binary); + if (!key_file_in) { + throw Error("Unable to open key file - have you unlocked/initialized this repository yet?"); + } + key_file.load(key_file_in); + } +} + // Encrypt contents of stdin and write to stdout -void clean (const char* keyfile) +int clean (int argc, char** argv) { - keys_t keys; - load_keys(keyfile, &keys); + const char* legacy_key_path = 0; + if (argc == 0) { + } else if (argc == 1) { + legacy_key_path = argv[0]; + } else { + std::clog << "Usage: git-crypt smudge" << std::endl; + return 2; + } + Key_file key_file; + load_key(key_file, legacy_key_path); + + const Key_file::Entry* key = key_file.get_latest(); + if (!key) { + std::clog << "git-crypt: error: key file is empty" << std::endl; + return 1; + } // Read the entire file - hmac_sha1_state hmac(keys.hmac, HMAC_KEY_LEN); // Calculate the file's SHA1 HMAC as we go - uint64_t file_size = 0; // Keep track of the length, make sure it doesn't get too big - std::string file_contents; // First 8MB or so of the file go here - std::fstream temp_file; // The rest of the file spills into a temporary file on disk + Hmac_sha1_state hmac(key->hmac_key, HMAC_KEY_LEN); // Calculate the file's SHA1 HMAC as we go + uint64_t file_size = 0; // Keep track of the length, make sure it doesn't get too big + std::string file_contents; // First 8MB or so of the file go here + std::fstream temp_file; // The rest of the file spills into a temporary file on disk temp_file.exceptions(std::fstream::badbit); - char buffer[1024]; + char buffer[1024]; - while (std::cin && file_size < MAX_CRYPT_BYTES) { + while (std::cin && file_size < Aes_ctr_encryptor::MAX_CRYPT_BYTES) { std::cin.read(buffer, sizeof(buffer)); size_t bytes_read = std::cin.gcount(); @@ -80,12 +157,11 @@ void clean (const char* keyfile) } // Make sure the file isn't so large we'll overflow the counter value (which would doom security) - if (file_size >= MAX_CRYPT_BYTES) { - std::clog << "File too long to encrypt securely\n"; - std::exit(1); + if (file_size >= Aes_ctr_encryptor::MAX_CRYPT_BYTES) { + std::clog << "git-crypt: error: file too long to encrypt securely" << std::endl; + return 1; } - // We use an HMAC of the file as the encryption nonce (IV) for CTR mode. // By using a hash of the file we ensure that the encryption is // deterministic so git doesn't think the file has changed when it really @@ -107,94 +183,177 @@ void clean (const char* keyfile) // looking up the nonce (which must be stored in the clear to allow for // decryption), we use an HMAC as opposed to a straight hash. - uint8_t digest[SHA1_LEN]; + // Note: Hmac_sha1_state::LEN >= Aes_ctr_encryptor::NONCE_LEN + + unsigned char digest[Hmac_sha1_state::LEN]; hmac.get(digest); // Write a header that... std::cout.write("\0GITCRYPT\0", 10); // ...identifies this as an encrypted file - std::cout.write(reinterpret_cast(digest), NONCE_LEN); // ...includes the nonce + std::cout.write(reinterpret_cast(digest), Aes_ctr_encryptor::NONCE_LEN); // ...includes the nonce // Now encrypt the file and write to stdout - aes_ctr_state state(digest, NONCE_LEN); + Aes_ctr_encryptor aes(key->aes_key, digest); // First read from the in-memory copy - const uint8_t* file_data = reinterpret_cast(file_contents.data()); - size_t file_data_len = file_contents.size(); - for (size_t i = 0; i < file_data_len; i += sizeof(buffer)) { - size_t buffer_len = std::min(sizeof(buffer), file_data_len - i); - state.process(&keys.enc, file_data + i, reinterpret_cast(buffer), buffer_len); + const unsigned char* file_data = reinterpret_cast(file_contents.data()); + size_t file_data_len = file_contents.size(); + while (file_data_len > 0) { + size_t buffer_len = std::min(sizeof(buffer), file_data_len); + aes.process(file_data, reinterpret_cast(buffer), buffer_len); std::cout.write(buffer, buffer_len); + file_data += buffer_len; + file_data_len -= buffer_len; } // Then read from the temporary file if applicable if (temp_file.is_open()) { temp_file.seekg(0); - while (temp_file) { + while (temp_file.peek() != -1) { temp_file.read(buffer, sizeof(buffer)); - size_t buffer_len = temp_file.gcount(); + size_t buffer_len = temp_file.gcount(); - state.process(&keys.enc, reinterpret_cast(buffer), reinterpret_cast(buffer), buffer_len); + aes.process(reinterpret_cast(buffer), + reinterpret_cast(buffer), + buffer_len); std::cout.write(buffer, buffer_len); } } + + return 0; } // Decrypt contents of stdin and write to stdout -void smudge (const char* keyfile) +int smudge (int argc, char** argv) { - keys_t keys; - load_keys(keyfile, &keys); + const char* legacy_key_path = 0; + if (argc == 0) { + } else if (argc == 1) { + legacy_key_path = argv[0]; + } else { + std::clog << "Usage: git-crypt smudge" << std::endl; + return 2; + } + Key_file key_file; + load_key(key_file, legacy_key_path); // Read the header to get the nonce and make sure it's actually encrypted - char header[22]; - std::cin.read(header, 22); - if (!std::cin || std::cin.gcount() != 22 || memcmp(header, "\0GITCRYPT\0", 10) != 0) { - std::clog << "File not encrypted\n"; - std::exit(1); + unsigned char header[10 + Aes_ctr_decryptor::NONCE_LEN]; + std::cin.read(reinterpret_cast(header), sizeof(header)); + if (!std::cin || std::cin.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) { + std::clog << "git-crypt: error: file not encrypted" << std::endl; + return 1; + } + const unsigned char* nonce = header + 10; + uint32_t key_version = 0; // TODO: get the version from the file header + + const Key_file::Entry* key = key_file.get(key_version); + if (!key) { + std::clog << "git-crypt: error: key version " << key_version << " not available - please unlock with the latest version of the key." << std::endl; + return 1; } - process_stream(std::cin, std::cout, &keys.enc, reinterpret_cast(header + 10)); + Aes_ctr_decryptor::process_stream(std::cin, std::cout, key->aes_key, nonce); + return 0; } -void diff (const char* keyfile, const char* filename) +int diff (int argc, char** argv) { - keys_t keys; - load_keys(keyfile, &keys); + const char* filename = 0; + const char* legacy_key_path = 0; + if (argc == 1) { + filename = argv[0]; + } else if (argc == 2) { + legacy_key_path = argv[0]; + filename = argv[1]; + } else { + std::clog << "Usage: git-crypt diff FILENAME" << std::endl; + return 2; + } + Key_file key_file; + load_key(key_file, legacy_key_path); // Open the file - std::ifstream in(filename); + std::ifstream in(filename, std::fstream::binary); if (!in) { - perror(filename); - std::exit(1); + std::clog << "git-crypt: " << filename << ": unable to open for reading" << std::endl; + return 1; } in.exceptions(std::fstream::badbit); // Read the header to get the nonce and determine if it's actually encrypted - char header[22]; - in.read(header, 22); - if (!in || in.gcount() != 22 || memcmp(header, "\0GITCRYPT\0", 10) != 0) { + unsigned char header[10 + Aes_ctr_decryptor::NONCE_LEN]; + in.read(reinterpret_cast(header), sizeof(header)); + if (!in || in.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) { // File not encrypted - just copy it out to stdout - std::cout.write(header, in.gcount()); // don't forget to include the header which we read! - char buffer[1024]; - while (in) { - in.read(buffer, sizeof(buffer)); - std::cout.write(buffer, in.gcount()); - } - return; + std::cout.write(reinterpret_cast(header), in.gcount()); // don't forget to include the header which we read! + std::cout << in.rdbuf(); + return 0; } - process_stream(in, std::cout, &keys.enc, reinterpret_cast(header + 10)); + // Go ahead and decrypt it + const unsigned char* nonce = header + 10; + uint32_t key_version = 0; // TODO: get the version from the file header + + const Key_file::Entry* key = key_file.get(key_version); + if (!key) { + std::clog << "git-crypt: error: key version " << key_version << " not available - please unlock with the latest version of the key." << std::endl; + return 1; + } + + Aes_ctr_decryptor::process_stream(in, std::cout, key->aes_key, nonce); + return 0; } - -void init (const char* argv0, const char* keyfile) +int init (int argc, char** argv) { - if (access(keyfile, R_OK) == -1) { - perror(keyfile); - std::exit(1); + if (argc == 1) { + std::clog << "Warning: 'git-crypt init' with a key file is deprecated as of git-crypt 0.4" << std::endl; + std::clog << "and will be removed in a future release. Please get in the habit of using" << std::endl; + std::clog << "'git-crypt unlock KEYFILE' instead." << std::endl; + return unlock(argc, argv); } - + if (argc != 0) { + std::clog << "Error: 'git-crypt init' takes no arguments." << std::endl; + return 2; + } + + std::string internal_key_path(get_internal_key_path()); + if (access(internal_key_path.c_str(), F_OK) == 0) { + // TODO: add a -f option to reinitialize the repo anyways (this should probably imply a refresh) + std::clog << "Error: this repository has already been initialized with git-crypt." << std::endl; + return 1; + } + + // 1. Generate a key and install it + std::clog << "Generating key..." << std::endl; + Key_file key_file; + key_file.generate(); + + mkdir_parent(internal_key_path); + if (!key_file.store(internal_key_path.c_str())) { + std::clog << "Error: " << internal_key_path << ": unable to write key file" << std::endl; + return 1; + } + + // 2. Configure git for git-crypt + configure_git_filters(); + + return 0; +} + +int unlock (int argc, char** argv) +{ + const char* symmetric_key_file = 0; + if (argc == 0) { + } else if (argc == 1) { + symmetric_key_file = argv[0]; + } else { + std::clog << "Usage: git-crypt unlock [KEYFILE]" << std::endl; + return 2; + } + // 0. Check to see if HEAD exists. See below why we do this. bool head_exists = system("git rev-parse HEAD >/dev/null 2>/dev/null") == 0; @@ -205,61 +364,67 @@ void init (const char* argv0, const char* keyfile) int status; std::stringstream status_output; status = exec_command("git status -uno --porcelain", status_output); - if (status != 0) { - std::clog << "git status failed - is this a git repository?\n"; - std::exit(1); + if (!successful_exit(status)) { + std::clog << "Error: 'git status' failed - is this a git repository?" << std::endl; + return 1; } else if (status_output.peek() != -1 && head_exists) { // We only care that the working directory is dirty if HEAD exists. // If HEAD doesn't exist, we won't be resetting to it (see below) so // it doesn't matter that the working directory is dirty. - std::clog << "Working directory not clean.\n"; - std::clog << "Please commit your changes or 'git stash' them before setting up git-crypt.\n"; - std::exit(1); + std::clog << "Error: Working directory not clean." << std::endl; + std::clog << "Please commit your changes or 'git stash' them before running 'git-crypt' unlock." << 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::stringstream cdup_output; - if (exec_command("git rev-parse --show-cdup", cdup_output) != 0) { - std::clog << "git rev-parse --show-cdup failed\n"; - std::exit(1); + if (!successful_exit(exec_command("git rev-parse --show-cdup", cdup_output))) { + std::clog << "Error: 'git rev-parse --show-cdup' failed" << std::endl; + return 1; } - // 3. Add config options to git - - std::string git_crypt_path(std::strchr(argv0, '/') ? resolve_path(argv0) : argv0); - std::string keyfile_path(resolve_path(keyfile)); - - // git config filter.git-crypt.smudge "git-crypt smudge /path/to/key" - std::string command("git config filter.git-crypt.smudge "); - command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " smudge " + escape_shell_arg(keyfile_path)); - - if (system(command.c_str()) != 0) { - std::clog << "git config failed\n"; - std::exit(1); + // 3. Install the key + Key_file key_file; + if (symmetric_key_file) { + // Read from the symmetric key file + try { + if (std::strcmp(symmetric_key_file, "-") == 0) { + key_file.load(std::cin); + } else { + if (!key_file.load(symmetric_key_file)) { + std::clog << "Error: " << symmetric_key_file << ": unable to read key file" << std::endl; + return 1; + } + } + } catch (Key_file::Incompatible) { + std::clog << "Error: " << symmetric_key_file << " is in an incompatible format" << std::endl; + std::clog << "Please upgrade to a newer version of git-crypt." << std::endl; + return 1; + } catch (Key_file::Malformed) { + std::clog << "Error: " << symmetric_key_file << ": not a valid git-crypt key file" << std::endl; + std::clog << "If this key was created prior to git-crypt 0.4, you need to migrate it" << std::endl; + std::clog << "by running 'git-crypt migrate-key /path/to/key/file'." << std::endl; + 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; + } + std::string internal_key_path(get_internal_key_path()); + // TODO: croak if internal_key_path already exists??? + mkdir_parent(internal_key_path); + if (!key_file.store(internal_key_path.c_str())) { + std::clog << "Error: " << internal_key_path << ": unable to write key file" << std::endl; + return 1; } - // git config filter.git-crypt.clean "git-crypt clean /path/to/key" - command = "git config filter.git-crypt.clean "; - command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " clean " + escape_shell_arg(keyfile_path)); - - if (system(command.c_str()) != 0) { - std::clog << "git config failed\n"; - std::exit(1); - } + // 4. Configure git for git-crypt + configure_git_filters(); - // git config diff.git-crypt.textconv "git-crypt diff /path/to/key" - command = "git config diff.git-crypt.textconv "; - command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " diff " + escape_shell_arg(keyfile_path)); - - if (system(command.c_str()) != 0) { - std::clog << "git config failed\n"; - std::exit(1); - } - - - // 4. Do a force checkout so any files that were previously checked out encrypted + // 5. Do a force checkout so any files that were previously checked out encrypted // will now be checked out decrypted. // If HEAD doesn't exist (perhaps because this repo doesn't have any files yet) // just skip the checkout. @@ -267,7 +432,7 @@ void init (const char* argv0, const char* keyfile) std::string path_to_top; std::getline(cdup_output, path_to_top); - command = "git checkout -f HEAD -- "; + std::string command("git checkout -f HEAD -- "); if (path_to_top.empty()) { command += "."; } else { @@ -275,37 +440,162 @@ void init (const char* argv0, const char* keyfile) } if (system(command.c_str()) != 0) { - std::clog << "git checkout failed\n"; - std::clog << "git-crypt has been set up but existing encrypted files have not been decrypted\n"; - std::exit(1); + std::clog << "Error: 'git checkout' failed" << std::endl; + std::clog << "git-crypt has been set up but existing encrypted files have not been decrypted" << std::endl; + return 1; } } + + return 0; } -void keygen (const char* keyfile) +int add_collab (int argc, char** argv) // TODO NOW { - if (access(keyfile, F_OK) == 0) { - std::clog << keyfile << ": File already exists - please remove before continuing\n"; - std::exit(1); - } - mode_t old_umask = umask(0077); // make sure key file is protected - std::ofstream keyout(keyfile); - if (!keyout) { - perror(keyfile); - std::exit(1); - } - umask(old_umask); - - std::clog << "Generating key...\n"; - std::clog.flush(); - unsigned char buffer[AES_KEY_BITS/8 + HMAC_KEY_LEN]; - if (RAND_bytes(buffer, sizeof(buffer)) != 1) { - while (unsigned long code = ERR_get_error()) { - char error_string[120]; - ERR_error_string_n(code, error_string, sizeof(error_string)); - std::clog << "Error: " << error_string << '\n'; - } - std::exit(1); - } - keyout.write(reinterpret_cast(buffer), sizeof(buffer)); + // 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; } + +int rm_collab (int argc, char** argv) // TODO +{ + std::clog << "Error: rm-collab is not yet implemented." << std::endl; + return 1; +} + +int ls_collabs (int argc, char** argv) // TODO +{ + // Sketch: + // Scan the sub-directories in .git-crypt/keys, outputting something like this: + // ==== + // Key version 0: + // 0x143DE9B3F7316900 Andrew Ayer + // 0x4E386D9C9C61702F ??? + // Key version 1: + // 0x143DE9B3F7316900 Andrew Ayer + // 0x1727274463D27F40 John Smith + // 0x4E386D9C9C61702F ??? + // ==== + // To resolve a long hex ID, use a command like this: + // gpg --options /dev/null --fixed-list-mode --batch --with-colons --list-keys 0x143DE9B3F7316900 + + std::clog << "Error: ls-collabs is not yet implemented." << std::endl; + return 1; +} + +int export_key (int argc, char** argv) +{ + // TODO: provide options to export only certain key versions + + if (argc != 1) { + std::clog << "Usage: git-crypt export-key FILENAME" << std::endl; + return 2; + } + + Key_file key_file; + load_key(key_file); + + const char* out_file_name = argv[0]; + + if (std::strcmp(out_file_name, "-") == 0) { + key_file.store(std::cout); + } else { + if (!key_file.store(out_file_name)) { + std::clog << "Error: " << out_file_name << ": unable to write key file" << std::endl; + return 1; + } + } + + return 0; +} + +int keygen (int argc, char** argv) +{ + if (argc != 1) { + std::clog << "Usage: git-crypt keygen KEYFILE" << std::endl; + return 2; + } + + const char* key_file_name = argv[0]; + + if (std::strcmp(key_file_name, "-") != 0 && access(key_file_name, F_OK) == 0) { + std::clog << key_file_name << ": File already exists" << std::endl; + return 1; + } + + std::clog << "Generating key..." << std::endl; + Key_file key_file; + key_file.generate(); + + if (std::strcmp(key_file_name, "-") == 0) { + key_file.store(std::cout); + } else { + if (!key_file.store(key_file_name)) { + std::clog << "Error: " << key_file_name << ": unable to write key file" << std::endl; + return 1; + } + } + return 0; +} + +int migrate_key (int argc, char** argv) +{ + if (argc != 1) { + std::clog << "Usage: git-crypt migrate-key KEYFILE" << std::endl; + return 2; + } + + const char* key_file_name = argv[0]; + Key_file key_file; + + try { + if (std::strcmp(key_file_name, "-") == 0) { + key_file.load_legacy(std::cin); + key_file.store(std::cout); + } else { + std::ifstream in(key_file_name, std::fstream::binary); + if (!in) { + std::clog << "Error: " << key_file_name << ": unable to open for reading" << std::endl; + return 1; + } + key_file.load_legacy(in); + in.close(); + + std::string new_key_file_name(key_file_name); + new_key_file_name += ".new"; + + if (access(new_key_file_name.c_str(), F_OK) == 0) { + std::clog << new_key_file_name << ": File already exists" << std::endl; + return 1; + } + + if (!key_file.store(new_key_file_name.c_str())) { + std::clog << "Error: " << new_key_file_name << ": unable to write key file" << std::endl; + return 1; + } + + if (rename(new_key_file_name.c_str(), key_file_name) == -1) { + std::clog << "Error: " << key_file_name << ": " << strerror(errno) << std::endl; + unlink(new_key_file_name.c_str()); + return 1; + } + } + } catch (Key_file::Malformed) { + std::clog << "Error: " << key_file_name << ": not a valid legacy git-crypt key file" << std::endl; + return 1; + } + + return 0; +} + +int refresh (int argc, char** argv) // TODO: do a force checkout, much like in unlock +{ + std::clog << "Error: refresh is not yet implemented." << std::endl; + return 1; +} + diff --git a/commands.hpp b/commands.hpp index ce68129..9772c0d 100644 --- a/commands.hpp +++ b/commands.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -31,12 +31,28 @@ #ifndef _COMMANDS_H #define _COMMANDS_H +#include -void clean (const char* keyfile); -void smudge (const char* keyfile); -void diff (const char* keyfile, const char* filename); -void init (const char* argv0, const char* keyfile); -void keygen (const char* keyfile); +struct Error { + std::string message; + + explicit Error (std::string m) : message(m) { } +}; + +// Plumbing commands: +int clean (int argc, char** argv); +int smudge (int argc, char** argv); +int diff (int argc, char** argv); +// Public commands: +int init (int argc, char** argv); +int unlock (int argc, char** argv); +int add_collab (int argc, char** argv); +int rm_collab (int argc, char** argv); +int ls_collabs (int argc, char** argv); +int export_key (int argc, char** argv); +int keygen (int argc, char** argv); +int migrate_key (int argc, char** argv); +int refresh (int argc, char** argv); #endif diff --git a/crypto.cpp b/crypto.cpp index 25c9ae0..c11d5e2 100644 --- a/crypto.cpp +++ b/crypto.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -34,80 +34,64 @@ #include #include #include -#include -#include +#include +#include +#include #include #include -void load_keys (const char* filepath, keys_t* keys) +Aes_ctr_encryptor::Aes_ctr_encryptor (const unsigned char* raw_key, const unsigned char* arg_nonce) { - std::ifstream file(filepath); - if (!file) { - perror(filepath); - std::exit(1); - } - char buffer[AES_KEY_BITS/8 + HMAC_KEY_LEN]; - file.read(buffer, sizeof(buffer)); - if (file.gcount() != sizeof(buffer)) { - std::clog << filepath << ": Premature end of key file\n"; - std::exit(1); + if (AES_set_encrypt_key(raw_key, KEY_LEN * 8, &key) != 0) { + throw Crypto_error("Aes_ctr_encryptor::Aes_ctr_encryptor", "AES_set_encrypt_key failed"); } - // First comes the AES encryption key - if (AES_set_encrypt_key(reinterpret_cast(buffer), AES_KEY_BITS, &keys->enc) != 0) { - std::clog << filepath << ": Failed to initialize AES encryption key\n"; - std::exit(1); - } - - // Then it's the HMAC key - memcpy(keys->hmac, buffer + AES_KEY_BITS/8, HMAC_KEY_LEN); -} - - -aes_ctr_state::aes_ctr_state (const uint8_t* arg_nonce, size_t arg_nonce_len) -{ - memset(nonce, '\0', sizeof(nonce)); - memcpy(nonce, arg_nonce, std::min(arg_nonce_len, sizeof(nonce))); + std::memcpy(nonce, arg_nonce, NONCE_LEN); byte_counter = 0; - memset(otp, '\0', sizeof(otp)); + std::memset(otp, '\0', sizeof(otp)); } -void aes_ctr_state::process (const AES_KEY* key, const uint8_t* in, uint8_t* out, size_t len) +void Aes_ctr_encryptor::process (const unsigned char* in, unsigned char* out, size_t len) { for (size_t i = 0; i < len; ++i) { - if (byte_counter % 16 == 0) { + if (byte_counter % BLOCK_LEN == 0) { + unsigned char ctr[BLOCK_LEN]; + + // First 12 bytes of CTR: nonce + std::memcpy(ctr, nonce, NONCE_LEN); + + // Last 4 bytes of CTR: block number (sequentially increasing with each block) (big endian) + store_be32(ctr + NONCE_LEN, byte_counter / BLOCK_LEN); + // Generate a new OTP - // CTR value: - // first 12 bytes - nonce - // last 4 bytes - block number (sequentially increasing with each block) - uint8_t ctr[16]; - uint32_t blockno = byte_counter / 16; - memcpy(ctr, nonce, 12); - store_be32(ctr + 12, blockno); - AES_encrypt(ctr, otp, key); + AES_encrypt(ctr, otp, &key); } // encrypt one byte - out[i] = in[i] ^ otp[byte_counter++ % 16]; + out[i] = in[i] ^ otp[byte_counter++ % BLOCK_LEN]; + + if (byte_counter == 0) { + throw Crypto_error("Aes_ctr_encryptor::process", "Too much data to encrypt securely"); + } } } -hmac_sha1_state::hmac_sha1_state (const uint8_t* key, size_t key_len) +Hmac_sha1_state::Hmac_sha1_state (const unsigned char* key, size_t key_len) { HMAC_Init(&ctx, key, key_len, EVP_sha1()); } -hmac_sha1_state::~hmac_sha1_state () +Hmac_sha1_state::~Hmac_sha1_state () { HMAC_cleanup(&ctx); } -void hmac_sha1_state::add (const uint8_t* buffer, size_t buffer_len) +void Hmac_sha1_state::add (const unsigned char* buffer, size_t buffer_len) { HMAC_Update(&ctx, buffer, buffer_len); } -void hmac_sha1_state::get (uint8_t* digest) +void Hmac_sha1_state::get (unsigned char* digest) { unsigned int len; HMAC_Final(&ctx, digest, &len); @@ -115,14 +99,28 @@ void hmac_sha1_state::get (uint8_t* digest) // Encrypt/decrypt an entire input stream, writing to the given output stream -void process_stream (std::istream& in, std::ostream& out, const AES_KEY* enc_key, const uint8_t* nonce) +void Aes_ctr_encryptor::process_stream (std::istream& in, std::ostream& out, const unsigned char* key, const unsigned char* nonce) { - aes_ctr_state state(nonce, 12); + Aes_ctr_encryptor aes(key, nonce); - uint8_t buffer[1024]; + unsigned char buffer[1024]; while (in) { in.read(reinterpret_cast(buffer), sizeof(buffer)); - state.process(enc_key, buffer, buffer, in.gcount()); + aes.process(buffer, buffer, in.gcount()); out.write(reinterpret_cast(buffer), in.gcount()); } } + +void random_bytes (unsigned char* buffer, size_t len) +{ + if (RAND_bytes(buffer, len) != 1) { + std::ostringstream message; + while (unsigned long code = ERR_get_error()) { + char error_string[120]; + ERR_error_string_n(code, error_string, sizeof(error_string)); + message << "OpenSSL Error: " << error_string << "; "; + } + throw Crypto_error("random_bytes", message.str()); + } +} + diff --git a/crypto.hpp b/crypto.hpp index e8166e2..69342bd 100644 --- a/crypto.hpp +++ b/crypto.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -31,53 +31,69 @@ #ifndef _CRYPTO_H #define _CRYPTO_H +#include "key.hpp" #include #include #include #include #include +#include -enum { - SHA1_LEN = 20, - NONCE_LEN = 12, - HMAC_KEY_LEN = 64, - AES_KEY_BITS = 256, - MAX_CRYPT_BYTES = (1ULL<<32)*16 // Don't encrypt more than this or the CTR value will repeat itself +struct Crypto_error { + std::string where; + std::string message; + + Crypto_error (const std::string& w, const std::string& m) : where(w), message(m) { } }; -struct keys_t { - AES_KEY enc; - uint8_t hmac[HMAC_KEY_LEN]; -}; -void load_keys (const char* filepath, keys_t* keys); +class Aes_ctr_encryptor { +public: + enum { + NONCE_LEN = 12, + KEY_LEN = AES_KEY_LEN, + BLOCK_LEN = 16, + MAX_CRYPT_BYTES = (1ULL<<32)*16 // Don't encrypt more than this or the CTR value will repeat itself + }; -class aes_ctr_state { +private: + AES_KEY key; char nonce[NONCE_LEN];// First 96 bits of counter uint32_t byte_counter; // How many bytes processed so far? - uint8_t otp[16]; // The current OTP that's in use + unsigned char otp[BLOCK_LEN]; // The current OTP that's in use public: - aes_ctr_state (const uint8_t* arg_nonce, size_t arg_nonce_len); + Aes_ctr_encryptor (const unsigned char* key, const unsigned char* nonce); - void process (const AES_KEY* key, const uint8_t* in, uint8_t* out, size_t len); + void process (const unsigned char* in, unsigned char* out, size_t len); + + // Encrypt/decrypt an entire input stream, writing to the given output stream + static void process_stream (std::istream& in, std::ostream& out, const unsigned char* key, const unsigned char* nonce); }; -class hmac_sha1_state { +typedef Aes_ctr_encryptor Aes_ctr_decryptor; + +class Hmac_sha1_state { +public: + enum { + LEN = 20, + KEY_LEN = HMAC_KEY_LEN + }; + +private: HMAC_CTX ctx; // disallow copy/assignment: - hmac_sha1_state (const hmac_sha1_state&) { } - hmac_sha1_state& operator= (const hmac_sha1_state&) { return *this; } -public: - hmac_sha1_state (const uint8_t* key, size_t key_len); - ~hmac_sha1_state (); + Hmac_sha1_state (const Hmac_sha1_state&) { } + Hmac_sha1_state& operator= (const Hmac_sha1_state&) { return *this; } - void add (const uint8_t* buffer, size_t buffer_len); - void get (uint8_t*); +public: + Hmac_sha1_state (const unsigned char* key, size_t key_len); + ~Hmac_sha1_state (); + + void add (const unsigned char* buffer, size_t buffer_len); + void get (unsigned char*); }; -// Encrypt/decrypt an entire input stream, writing to the given output stream -void process_stream (std::istream& in, std::ostream& out, const AES_KEY* enc_key, const uint8_t* nonce); - +void random_bytes (unsigned char*, size_t); #endif diff --git a/git-crypt.cpp b/git-crypt.cpp index bd58391..69d7fed 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -28,29 +28,59 @@ * as that of the covered work. */ +#include "git-crypt.hpp" #include "commands.hpp" #include "util.hpp" +#include "crypto.hpp" +#include "key.hpp" #include +#include #include +#include #include -static void print_usage (const char* argv0) +const char* argv0; + +static void print_usage (std::ostream& out) { - std::clog << "Usage: " << argv0 << " COMMAND [ARGS ...]\n"; - std::clog << "\n"; - std::clog << "Valid commands:\n"; - std::clog << " init KEYFILE - prepare the current git repo to use git-crypt with this key\n"; - std::clog << " keygen KEYFILE - generate a git-crypt key in the given file\n"; - std::clog << "\n"; - std::clog << "Plumbing commands (not to be used directly):\n"; - std::clog << " clean KEYFILE\n"; - std::clog << " smudge KEYFILE\n"; - std::clog << " diff KEYFILE FILE\n"; + out << "Usage: " << argv0 << " COMMAND [ARGS ...]" << std::endl; + out << "" << std::endl; + out << "Standard commands:" << std::endl; + out << " init - generate a key, prepare the current repo to use git-crypt" << std::endl; + out << " unlock KEYFILE - decrypt the current repo using the given symmetric key" << std::endl; + out << " export-key FILE - export the repo's symmetric key to the given file" << std::endl; + //out << " refresh - ensure all files in the repo are properly decrypted" << std::endl; + out << " help - display this help message" << std::endl; + out << " help COMMAND - display help for the given git-crypt command" << std::endl; + out << "" << std::endl; + /* + out << "GPG commands:" << std::endl; + out << " unlock - decrypt the current repo using the in-repo GPG-encrypted key" << std::endl; + out << " add-collab GPGID - add the user with the given GPG key ID as a collaborator" << std::endl; + out << " rm-collab GPGID - revoke collaborator status from the given GPG key ID" << std::endl; + out << " ls-collabs - list the GPG key IDs of collaborators" << std::endl; + out << "" << std::endl; + */ + out << "Legacy commands:" << std::endl; + out << " init KEYFILE - alias for 'unlock KEYFILE'" << std::endl; + out << " keygen KEYFILE - generate a git-crypt key in the given file" << std::endl; + out << " migrate-key FILE - migrate the given legacy key file to the latest format" << std::endl; + out << "" << std::endl; + out << "Plumbing commands (not to be used directly):" << std::endl; + out << " clean [LEGACY-KEYFILE]" << std::endl; + out << " smudge [LEGACY-KEYFILE]" << std::endl; + out << " diff [LEGACY-KEYFILE] FILE" << std::endl; } -int main (int argc, const char** argv) +int main (int argc, char** argv) try { + argv0 = argv[0]; + + /* + * General initialization + */ + // The following two lines are essential for achieving good performance: std::ios_base::sync_with_stdio(false); std::cin.tie(0); @@ -58,29 +88,115 @@ try { std::cin.exceptions(std::ios_base::badbit); std::cout.exceptions(std::ios_base::badbit); - if (argc < 3) { - print_usage(argv[0]); - return 2; - } - ERR_load_crypto_strings(); - if (strcmp(argv[1], "init") == 0 && argc == 3) { - init(argv[0], argv[2]); - } else if (strcmp(argv[1], "keygen") == 0 && argc == 3) { - keygen(argv[2]); - } else if (strcmp(argv[1], "clean") == 0 && argc == 3) { - clean(argv[2]); - } else if (strcmp(argv[1], "smudge") == 0 && argc == 3) { - smudge(argv[2]); - } else if (strcmp(argv[1], "diff") == 0 && argc == 4) { - diff(argv[2], argv[3]); - } else { - print_usage(argv[0]); + /* + * Parse command line arguments + */ + const char* profile = 0; + int arg_index = 1; + while (arg_index < argc && argv[arg_index][0] == '-') { + if (std::strcmp(argv[arg_index], "--help") == 0) { + print_usage(std::clog); + return 0; + } else if (std::strncmp(argv[arg_index], "--profile=", 10) == 0) { + profile = argv[arg_index] + 10; + ++arg_index; + } else if (std::strcmp(argv[arg_index], "-p") == 0 && arg_index + 1 < argc) { + profile = argv[arg_index + 1]; + arg_index += 2; + } else if (std::strcmp(argv[arg_index], "--") == 0) { + ++arg_index; + break; + } else { + std::clog << argv0 << ": " << argv[arg_index] << ": Unknown option" << std::endl; + print_usage(std::clog); + return 2; + } + } + + (void)(profile); // TODO: profile support + + argc -= arg_index; + argv += arg_index; + + if (argc == 0) { + print_usage(std::clog); return 2; } - return 0; + /* + * Pass off to command handler + */ + const char* command = argv[0]; + --argc; + ++argv; + + // Public commands: + if (std::strcmp(command, "help") == 0) { + print_usage(std::clog); + return 0; + } + if (std::strcmp(command, "init") == 0) { + return init(argc, argv); + } + if (std::strcmp(command, "unlock") == 0) { + return unlock(argc, argv); + } + if (std::strcmp(command, "add-collab") == 0) { + return add_collab(argc, argv); + } + if (std::strcmp(command, "rm-collab") == 0) { + return rm_collab(argc, argv); + } + if (std::strcmp(command, "ls-collabs") == 0) { + return ls_collabs(argc, argv); + } + if (std::strcmp(command, "export-key") == 0) { + return export_key(argc, argv); + } + if (std::strcmp(command, "keygen") == 0) { + return keygen(argc, argv); + } + if (std::strcmp(command, "migrate-key") == 0) { + return migrate_key(argc, argv); + } + if (std::strcmp(command, "refresh") == 0) { + return refresh(argc, argv); + } + // Plumbing commands (executed by git, not by user): + if (std::strcmp(command, "clean") == 0) { + return clean(argc, argv); + } + if (std::strcmp(command, "smudge") == 0) { + return smudge(argc, argv); + } + if (std::strcmp(command, "diff") == 0) { + return diff(argc, argv); + } + + print_usage(std::clog); + return 2; + +} catch (const Error& e) { + std::cerr << "git-crypt: Error: " << e.message << std::endl; + return 1; +} catch (const System_error& e) { + std::cerr << "git-crypt: " << e.action << ": "; + if (!e.target.empty()) { + std::cerr << e.target << ": "; + } + std::cerr << strerror(e.error) << std::endl; + return 1; +} catch (const Crypto_error& e) { + std::cerr << "git-crypt: Crypto error: " << e.where << ": " << e.message << std::endl; + return 1; +} catch (Key_file::Incompatible) { + std::cerr << "git-crypt: This repository contains a incompatible key file. Please upgrade git-crypt." << std::endl; + return 1; +} catch (Key_file::Malformed) { + std::cerr << "git-crypt: This repository contains a malformed key file. It may be corrupted." << std::endl; + return 1; } catch (const std::ios_base::failure& e) { std::cerr << "git-crypt: I/O error: " << e.what() << std::endl; return 1; diff --git a/git-crypt.hpp b/git-crypt.hpp new file mode 100644 index 0000000..00091d5 --- /dev/null +++ b/git-crypt.hpp @@ -0,0 +1,36 @@ +/* + * 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 _GIT_CRYPT_H +#define _GIT_CRYPT_H + +extern const char* argv0; // initialized in main() to argv[0] + +#endif diff --git a/key.cpp b/key.cpp new file mode 100644 index 0000000..c71be60 --- /dev/null +++ b/key.cpp @@ -0,0 +1,161 @@ +/* + * 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 "key.hpp" +#include "util.hpp" +#include "crypto.hpp" +#include +#include +#include +#include +#include +#include +#include + +void Key_file::Entry::load (std::istream& in) +{ + // First comes the AES key + in.read(reinterpret_cast(aes_key), AES_KEY_LEN); + if (in.gcount() != AES_KEY_LEN) { + throw Malformed(); + } + + // Then the HMAC key + in.read(reinterpret_cast(hmac_key), HMAC_KEY_LEN); + if (in.gcount() != HMAC_KEY_LEN) { + throw Malformed(); + } +} + +void Key_file::Entry::store (std::ostream& out) const +{ + out.write(reinterpret_cast(aes_key), AES_KEY_LEN); + out.write(reinterpret_cast(hmac_key), HMAC_KEY_LEN); +} + +void Key_file::Entry::generate () +{ + random_bytes(aes_key, AES_KEY_LEN); + random_bytes(hmac_key, HMAC_KEY_LEN); +} + +const Key_file::Entry* Key_file::get_latest () const +{ + return is_filled() ? get(latest()) : 0; +} + +const Key_file::Entry* Key_file::get (uint32_t version) const +{ + Map::const_iterator it(entries.find(version)); + return it != entries.end() ? &it->second : 0; +} + +void Key_file::add (uint32_t version, const Entry& entry) +{ + entries[version] = entry; +} + + +void Key_file::load_legacy (std::istream& in) +{ + entries[0].load(in); +} + +void Key_file::load (std::istream& in) +{ + unsigned char preamble[16]; + in.read(reinterpret_cast(preamble), 16); + if (in.gcount() != 16) { + throw Malformed(); + } + if (std::memcmp(preamble, "\0GITCRYPTKEY", 12) != 0) { + throw Malformed(); + } + if (load_be32(preamble + 12) != FORMAT_VERSION) { + throw Incompatible(); + } + while (in.peek() != -1) { + uint32_t version; + if (!read_be32(in, version)) { + throw Malformed(); + } + entries[version].load(in); + } +} + +void Key_file::store (std::ostream& out) const +{ + out.write("\0GITCRYPTKEY", 12); + write_be32(out, FORMAT_VERSION); + for (Map::const_iterator it(entries.begin()); it != entries.end(); ++it) { + write_be32(out, it->first); + it->second.store(out); + } +} + +bool Key_file::load (const char* key_file_name) +{ + std::ifstream key_file_in(key_file_name, std::fstream::binary); + if (!key_file_in) { + return false; + } + load(key_file_in); + return true; +} + +bool Key_file::store (const char* key_file_name) const +{ + mode_t old_umask = umask(0077); // make sure key file is protected + std::ofstream key_file_out(key_file_name, std::fstream::binary); + umask(old_umask); + if (!key_file_out) { + return false; + } + store(key_file_out); + key_file_out.close(); + if (!key_file_out) { + return false; + } + return true; +} + +void Key_file::generate () +{ + entries[is_empty() ? 0 : latest() + 1].generate(); +} + +uint32_t Key_file::latest () const +{ + if (is_empty()) { + throw std::invalid_argument("Key_file::latest"); + } + return entries.begin()->first; +} + diff --git a/key.hpp b/key.hpp new file mode 100644 index 0000000..db49154 --- /dev/null +++ b/key.hpp @@ -0,0 +1,84 @@ +/* + * 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 _KEY_H +#define _KEY_H + +#include +#include +#include +#include + +enum { + HMAC_KEY_LEN = 64, + AES_KEY_LEN = 32 +}; + +struct Key_file { +public: + struct Entry { + unsigned char aes_key[AES_KEY_LEN]; + unsigned char hmac_key[HMAC_KEY_LEN]; + + void load (std::istream&); + void store (std::ostream&) const; + void generate (); + }; + + struct Malformed { }; // exception class + struct Incompatible { }; // exception class + + const Entry* get_latest () const; + + const Entry* get (uint32_t version) const; + void add (uint32_t version, const Entry&); + + void load_legacy (std::istream&); + void load (std::istream&); + void store (std::ostream&) const; + + bool load (const char* filename); + bool store (const char* filename) const; + + void generate (); + + bool is_empty () const { return entries.empty(); } + bool is_filled () const { return !is_empty(); } + + uint32_t latest () const; + +private: + typedef std::map > Map; + enum { FORMAT_VERSION = 1 }; + + Map entries; +}; + +#endif diff --git a/util.cpp b/util.cpp index e37d7cc..546b4ab 100644 --- a/util.cpp +++ b/util.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -28,8 +28,10 @@ * as that of the covered work. */ +#include "git-crypt.hpp" #include "util.hpp" #include +#include #include #include #include @@ -40,17 +42,77 @@ #include #include +void mkdir_parent (const std::string& path) +{ + std::string::size_type slash(path.find('/', 1)); + while (slash != std::string::npos) { + std::string prefix(path.substr(0, slash)); + struct stat status; + if (stat(prefix.c_str(), &status) == 0) { + // already exists - make sure it's a directory + if (!S_ISDIR(status.st_mode)) { + throw System_error("mkdir_parent", prefix, ENOTDIR); + } + } else { + if (errno != ENOENT) { + throw System_error("mkdir_parent", prefix, errno); + } + // doesn't exist - mkdir it + if (mkdir(prefix.c_str(), 0777) == -1) { + throw System_error("mkdir", prefix, errno); + } + } + + slash = path.find('/', slash + 1); + } +} + +std::string readlink (const char* pathname) +{ + std::vector buffer(64); + ssize_t len; + + while ((len = ::readlink(pathname, &buffer[0], buffer.size())) == static_cast(buffer.size())) { + // buffer may have been truncated - grow and try again + buffer.resize(buffer.size() * 2); + } + if (len == -1) { + throw System_error("readlink", pathname, errno); + } + + return std::string(buffer.begin(), buffer.begin() + len); +} + +std::string our_exe_path () +{ + try { + return readlink("/proc/self/exe"); + } catch (const System_error&) { + if (argv0[0] == '/') { + // argv[0] starts with / => it's an absolute path + return argv0; + } else if (std::strchr(argv0, '/')) { + // argv[0] contains / => it a relative path that should be resolved + char* resolved_path_p = realpath(argv0, NULL); + std::string resolved_path(resolved_path_p); + free(resolved_path_p); + return resolved_path; + } else { + // argv[0] is just a bare filename => not much we can do + return argv0; + } + } +} + int exec_command (const char* command, std::ostream& output) { int pipefd[2]; if (pipe(pipefd) == -1) { - perror("pipe"); - std::exit(9); + throw System_error("pipe", "", errno); } pid_t child = fork(); if (child == -1) { - perror("fork"); - std::exit(9); + throw System_error("fork", "", errno); } if (child == 0) { close(pipefd[0]); @@ -59,7 +121,8 @@ int exec_command (const char* command, std::ostream& output) close(pipefd[1]); } execl("/bin/sh", "sh", "-c", command, NULL); - exit(-1); + perror("/bin/sh"); + _exit(-1); } close(pipefd[1]); char buffer[1024]; @@ -67,49 +130,53 @@ int exec_command (const char* command, std::ostream& output) 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; - waitpid(child, &status, 0); + if (waitpid(child, &status, 0) == -1) { + throw System_error("waitpid", "", errno); + } return status; } -std::string resolve_path (const char* path) +bool successful_exit (int status) { - char* resolved_path_p = realpath(path, NULL); - std::string resolved_path(resolved_path_p); - free(resolved_path_p); - return resolved_path; + return WIFEXITED(status) && WEXITSTATUS(status) == 0; } void open_tempfile (std::fstream& file, std::ios_base::openmode mode) { - const char* tmpdir = getenv("TMPDIR"); - size_t tmpdir_len; - if (tmpdir) { - tmpdir_len = strlen(tmpdir); - } else { + const char* tmpdir = getenv("TMPDIR"); + size_t tmpdir_len = tmpdir ? std::strlen(tmpdir) : 0; + if (tmpdir_len == 0 || tmpdir_len > 4096) { + // no $TMPDIR or it's excessively long => fall back to /tmp tmpdir = "/tmp"; tmpdir_len = 4; } - char* path = new char[tmpdir_len + 18]; - strcpy(path, tmpdir); - strcpy(path + tmpdir_len, "/git-crypt.XXXXXX"); - mode_t old_umask = umask(0077); - int fd = mkstemp(path); + std::vector path_buffer(tmpdir_len + 18); + char* path = &path_buffer[0]; + std::strcpy(path, tmpdir); + std::strcpy(path + tmpdir_len, "/git-crypt.XXXXXX"); + mode_t old_umask = umask(0077); + int fd = mkstemp(path); if (fd == -1) { - perror("mkstemp"); - std::exit(9); + int mkstemp_errno = errno; + umask(old_umask); + throw System_error("mkstemp", "", mkstemp_errno); } umask(old_umask); file.open(path, mode); if (!file.is_open()) { - perror("open"); unlink(path); - std::exit(9); + close(fd); + throw System_error("std::fstream::open", path, 0); } unlink(path); close(fd); - delete[] path; } std::string escape_shell_arg (const std::string& str) diff --git a/util.hpp b/util.hpp index 2bd7356..37fa523 100644 --- a/util.hpp +++ b/util.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -36,8 +36,19 @@ #include #include +struct System_error { + std::string action; + std::string target; + int error; + + System_error (const std::string& a, const std::string& t, int e) : action(a), target(t), error(e) { } +}; + +void mkdir_parent (const std::string& path); // Create parent directories of path, __but not path itself__ +std::string readlink (const char* pathname); +std::string our_exe_path (); int exec_command (const char* command, std::ostream& output); -std::string resolve_path (const char* path); +bool successful_exit (int status); void open_tempfile (std::fstream&, std::ios_base::openmode); std::string escape_shell_arg (const std::string&); uint32_t load_be32 (const unsigned char*); From cd5f3534aac9d45340153e9e701185278e228b4c Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Fri, 28 Mar 2014 13:51:10 -0700 Subject: [PATCH 03/47] Rename some functions instead of overloading them It's more clear this way. --- commands.cpp | 12 ++++++------ key.cpp | 4 ++-- key.hpp | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/commands.cpp b/commands.cpp index a0fc292..600b78c 100644 --- a/commands.cpp +++ b/commands.cpp @@ -332,7 +332,7 @@ int init (int argc, char** argv) key_file.generate(); mkdir_parent(internal_key_path); - if (!key_file.store(internal_key_path.c_str())) { + if (!key_file.store_to_file(internal_key_path.c_str())) { std::clog << "Error: " << internal_key_path << ": unable to write key file" << std::endl; return 1; } @@ -393,7 +393,7 @@ int unlock (int argc, char** argv) if (std::strcmp(symmetric_key_file, "-") == 0) { key_file.load(std::cin); } else { - if (!key_file.load(symmetric_key_file)) { + if (!key_file.load_from_file(symmetric_key_file)) { std::clog << "Error: " << symmetric_key_file << ": unable to read key file" << std::endl; return 1; } @@ -416,7 +416,7 @@ int unlock (int argc, char** argv) std::string internal_key_path(get_internal_key_path()); // TODO: croak if internal_key_path already exists??? mkdir_parent(internal_key_path); - if (!key_file.store(internal_key_path.c_str())) { + if (!key_file.store_to_file(internal_key_path.c_str())) { std::clog << "Error: " << internal_key_path << ": unable to write key file" << std::endl; return 1; } @@ -505,7 +505,7 @@ int export_key (int argc, char** argv) if (std::strcmp(out_file_name, "-") == 0) { key_file.store(std::cout); } else { - if (!key_file.store(out_file_name)) { + if (!key_file.store_to_file(out_file_name)) { std::clog << "Error: " << out_file_name << ": unable to write key file" << std::endl; return 1; } @@ -535,7 +535,7 @@ int keygen (int argc, char** argv) if (std::strcmp(key_file_name, "-") == 0) { key_file.store(std::cout); } else { - if (!key_file.store(key_file_name)) { + if (!key_file.store_to_file(key_file_name)) { std::clog << "Error: " << key_file_name << ": unable to write key file" << std::endl; return 1; } @@ -574,7 +574,7 @@ int migrate_key (int argc, char** argv) return 1; } - if (!key_file.store(new_key_file_name.c_str())) { + if (!key_file.store_to_file(new_key_file_name.c_str())) { std::clog << "Error: " << new_key_file_name << ": unable to write key file" << std::endl; return 1; } diff --git a/key.cpp b/key.cpp index c71be60..bf39261 100644 --- a/key.cpp +++ b/key.cpp @@ -120,7 +120,7 @@ void Key_file::store (std::ostream& out) const } } -bool Key_file::load (const char* key_file_name) +bool Key_file::load_from_file (const char* key_file_name) { std::ifstream key_file_in(key_file_name, std::fstream::binary); if (!key_file_in) { @@ -130,7 +130,7 @@ bool Key_file::load (const char* key_file_name) return true; } -bool Key_file::store (const char* key_file_name) const +bool Key_file::store_to_file (const char* key_file_name) const { mode_t old_umask = umask(0077); // make sure key file is protected std::ofstream key_file_out(key_file_name, std::fstream::binary); diff --git a/key.hpp b/key.hpp index db49154..d16241f 100644 --- a/key.hpp +++ b/key.hpp @@ -64,8 +64,8 @@ public: void load (std::istream&); void store (std::ostream&) const; - bool load (const char* filename); - bool store (const char* filename) const; + bool load_from_file (const char* filename); + bool store_to_file (const char* filename) const; void generate (); From df838947a0fbfe39933b95c0468d45b7a4fea63c Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Fri, 28 Mar 2014 13:52:33 -0700 Subject: [PATCH 04/47] Use successful_exit() helper for testing system() return value --- commands.cpp | 10 +++++----- util.cpp | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/commands.cpp b/commands.cpp index 600b78c..ee48b3e 100644 --- a/commands.cpp +++ b/commands.cpp @@ -55,7 +55,7 @@ static void configure_git_filters () std::string command("git config filter.git-crypt.smudge "); command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " smudge"); - if (system(command.c_str()) != 0) { + if (!successful_exit(system(command.c_str()))) { throw Error("'git config' failed"); } @@ -63,7 +63,7 @@ static void configure_git_filters () command = "git config filter.git-crypt.clean "; command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " clean"); - if (system(command.c_str()) != 0) { + if (!successful_exit(system(command.c_str()))) { throw Error("'git config' failed"); } @@ -71,7 +71,7 @@ static void configure_git_filters () command = "git config diff.git-crypt.textconv "; command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " diff"); - if (system(command.c_str()) != 0) { + if (!successful_exit(system(command.c_str()))) { throw Error("'git config' failed"); } } @@ -355,7 +355,7 @@ int unlock (int argc, char** argv) } // 0. Check to see if HEAD exists. See below why we do this. - bool head_exists = system("git rev-parse HEAD >/dev/null 2>/dev/null") == 0; + bool head_exists = successful_exit(system("git rev-parse HEAD >/dev/null 2>/dev/null")); // 1. Make sure working directory is clean (ignoring untracked files) // We do this because we run 'git checkout -f HEAD' later and we don't @@ -439,7 +439,7 @@ int unlock (int argc, char** argv) command += escape_shell_arg(path_to_top); } - if (system(command.c_str()) != 0) { + if (!successful_exit(system(command.c_str()))) { std::clog << "Error: 'git checkout' failed" << std::endl; std::clog << "git-crypt has been set up but existing encrypted files have not been decrypted" << std::endl; return 1; diff --git a/util.cpp b/util.cpp index 546b4ab..d865be4 100644 --- a/util.cpp +++ b/util.cpp @@ -145,7 +145,7 @@ int exec_command (const char* command, std::ostream& output) bool successful_exit (int status) { - return WIFEXITED(status) && WEXITSTATUS(status) == 0; + return status != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0; } void open_tempfile (std::fstream& file, std::ios_base::openmode mode) From b2bdc11330ea5b984cdaec8b22b2f4968daafe69 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Fri, 28 Mar 2014 13:53:12 -0700 Subject: [PATCH 05/47] Fix a typo in an error message --- commands.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands.cpp b/commands.cpp index ee48b3e..f7b60f3 100644 --- a/commands.cpp +++ b/commands.cpp @@ -81,7 +81,7 @@ static std::string get_internal_key_path () std::stringstream output; if (!successful_exit(exec_command("git rev-parse --git-dir", output))) { - throw Error("'git rev-parse --git-dir' - is this a Git repository?"); + throw Error("'git rev-parse --git-dir' failed - is this a Git repository?"); } std::string path; From 2b5e4a752edf4ab7fac19b24d5512d9861adf25f Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Fri, 28 Mar 2014 13:54:18 -0700 Subject: [PATCH 06/47] Plug a file descriptor leak if fork() fails (Not that we really care if that happens ;-) but it's good to be correct.) --- util.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/util.cpp b/util.cpp index d865be4..2afd685 100644 --- a/util.cpp +++ b/util.cpp @@ -112,7 +112,10 @@ int exec_command (const char* command, std::ostream& output) } pid_t child = fork(); if (child == -1) { - throw System_error("fork", "", errno); + int fork_errno = errno; + close(pipefd[0]); + close(pipefd[1]); + throw System_error("fork", "", fork_errno); } if (child == 0) { close(pipefd[0]); From 7687d112190c65cb180d53eb52d46a2f6b184f83 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Fri, 28 Mar 2014 14:02:25 -0700 Subject: [PATCH 07/47] 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&); From 8c77209d40c5c0027041e3b6acbe55e5f7b5bbfe Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Tue, 1 Apr 2014 16:18:28 -0700 Subject: [PATCH 08/47] Fix include guards to not start with _ Since such names are reserved, technically. --- commands.hpp | 4 ++-- crypto.hpp | 4 ++-- git-crypt.hpp | 4 ++-- gpg.hpp | 4 ++-- key.hpp | 4 ++-- util.hpp | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/commands.hpp b/commands.hpp index 9772c0d..33d674b 100644 --- a/commands.hpp +++ b/commands.hpp @@ -28,8 +28,8 @@ * as that of the covered work. */ -#ifndef _COMMANDS_H -#define _COMMANDS_H +#ifndef GIT_CRYPT_COMMANDS_HPP +#define GIT_CRYPT_COMMANDS_HPP #include diff --git a/crypto.hpp b/crypto.hpp index 69342bd..63772a1 100644 --- a/crypto.hpp +++ b/crypto.hpp @@ -28,8 +28,8 @@ * as that of the covered work. */ -#ifndef _CRYPTO_H -#define _CRYPTO_H +#ifndef GIT_CRYPT_CRYPTO_HPP +#define GIT_CRYPT_CRYPTO_HPP #include "key.hpp" #include diff --git a/git-crypt.hpp b/git-crypt.hpp index 00091d5..94b1ded 100644 --- a/git-crypt.hpp +++ b/git-crypt.hpp @@ -28,8 +28,8 @@ * as that of the covered work. */ -#ifndef _GIT_CRYPT_H -#define _GIT_CRYPT_H +#ifndef GIT_CRYPT_GIT_CRYPT_HPP +#define GIT_CRYPT_GIT_CRYPT_HPP extern const char* argv0; // initialized in main() to argv[0] diff --git a/gpg.hpp b/gpg.hpp index c2939bb..cd55171 100644 --- a/gpg.hpp +++ b/gpg.hpp @@ -28,8 +28,8 @@ * as that of the covered work. */ -#ifndef _GPG_H -#define _GPG_H +#ifndef GIT_CRYPT_GPG_HPP +#define GIT_CRYPT_GPG_HPP #include #include diff --git a/key.hpp b/key.hpp index c237d30..30a4216 100644 --- a/key.hpp +++ b/key.hpp @@ -28,8 +28,8 @@ * as that of the covered work. */ -#ifndef _KEY_H -#define _KEY_H +#ifndef GIT_CRYPT_KEY_HPP +#define GIT_CRYPT_KEY_HPP #include #include diff --git a/util.hpp b/util.hpp index 9c45095..c86e53d 100644 --- a/util.hpp +++ b/util.hpp @@ -28,8 +28,8 @@ * as that of the covered work. */ -#ifndef _UTIL_H -#define _UTIL_H +#ifndef GIT_CRYPT_UTIL_HPP +#define GIT_CRYPT_UTIL_HPP #include #include From c2a9e48de5668cd59a2b0b54c0b819e0e9d58559 Mon Sep 17 00:00:00 2001 From: Simon Kotlinski Date: Tue, 3 Jun 2014 13:17:16 +0200 Subject: [PATCH 09/47] Makefile: don't compile with -ansi Fixes build on Cygwin due to [1]. Closes #19 on GitHub. [1] https://cygwin.com/ml/cygwin/2014-01/msg00130.html --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5b48350..a336360 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ CXX := c++ -CXXFLAGS := -Wall -pedantic -ansi -Wno-long-long -O2 +CXXFLAGS := -Wall -pedantic -Wno-long-long -O2 LDFLAGS := -lcrypto PREFIX := /usr/local From 0774ed018c4869d3db047dda4dbceca1b1dfa9a9 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sun, 8 Jun 2014 16:03:18 -0700 Subject: [PATCH 10/47] Lay groundwork for Windows support Move Unix-specific code to util-unix.cpp, and place Windows equivalents in util-win32.cpp. Most of the Windows functions are just stubs at the moment, and we need a build system that works on Windows. --- Makefile | 2 + commands.cpp | 6 +- git-crypt.cpp | 14 +-- key.cpp | 3 +- util-unix.cpp | 250 +++++++++++++++++++++++++++++++++++++++++++++++++ util-win32.cpp | 124 ++++++++++++++++++++++++ util.cpp | 212 ++++------------------------------------- util.hpp | 15 ++- 8 files changed, 415 insertions(+), 211 deletions(-) create mode 100644 util-unix.cpp create mode 100644 util-win32.cpp diff --git a/Makefile b/Makefile index a336360..2de93f9 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,8 @@ all: git-crypt git-crypt: $(OBJFILES) $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) +util.o: util.cpp util-unix.cpp util-win32.cpp + clean: rm -f *.o git-crypt diff --git a/commands.cpp b/commands.cpp index 7c859a7..1cc47ef 100644 --- a/commands.cpp +++ b/commands.cpp @@ -33,8 +33,6 @@ #include "util.hpp" #include "key.hpp" #include "gpg.hpp" -#include -#include #include #include #include @@ -202,7 +200,7 @@ int clean (int argc, char** argv) Hmac_sha1_state hmac(key->hmac_key, HMAC_KEY_LEN); // Calculate the file's SHA1 HMAC as we go uint64_t file_size = 0; // Keep track of the length, make sure it doesn't get too big std::string file_contents; // First 8MB or so of the file go here - std::fstream temp_file; // The rest of the file spills into a temporary file on disk + temp_fstream temp_file; // The rest of the file spills into a temporary file on disk temp_file.exceptions(std::fstream::badbit); char buffer[1024]; @@ -219,7 +217,7 @@ int clean (int argc, char** argv) file_contents.append(buffer, bytes_read); } else { if (!temp_file.is_open()) { - open_tempfile(temp_file, std::fstream::in | std::fstream::out | std::fstream::binary | std::fstream::app); + temp_file.open(std::fstream::in | std::fstream::out | std::fstream::binary | std::fstream::app); } temp_file.write(buffer, bytes_read); } diff --git a/git-crypt.cpp b/git-crypt.cpp index 8bed1a0..aaf27fb 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -82,13 +82,7 @@ try { * General initialization */ - // The following two lines are essential for achieving good performance: - std::ios_base::sync_with_stdio(false); - std::cin.tie(0); - - std::cin.exceptions(std::ios_base::badbit); - std::cout.exceptions(std::ios_base::badbit); - + init_std_streams(); ERR_load_crypto_strings(); /* @@ -186,11 +180,7 @@ try { 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()) { - std::cerr << e.target << ": "; - } - std::cerr << strerror(e.error) << std::endl; + std::cerr << "git-crypt: System error: " << e.message() << std::endl; return 1; } catch (const Crypto_error& e) { std::cerr << "git-crypt: Crypto error: " << e.where << ": " << e.message << std::endl; diff --git a/key.cpp b/key.cpp index 2fea653..508ff8d 100644 --- a/key.cpp +++ b/key.cpp @@ -33,6 +33,7 @@ #include "crypto.hpp" #include #include +#include #include #include #include @@ -133,7 +134,7 @@ bool Key_file::load_from_file (const char* key_file_name) bool Key_file::store_to_file (const char* key_file_name) const { - mode_t old_umask = umask(0077); // make sure key file is protected + mode_t old_umask = umask(0077); // make sure key file is protected (TODO: Windows compat) std::ofstream key_file_out(key_file_name, std::fstream::binary); umask(old_umask); if (!key_file_out) { diff --git a/util-unix.cpp b/util-unix.cpp new file mode 100644 index 0000000..214f501 --- /dev/null +++ b/util-unix.cpp @@ -0,0 +1,250 @@ +/* + * Copyright 2012, 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +std::string System_error::message () const +{ + std::string mesg(action); + if (!target.empty()) { + mesg += ": "; + mesg += target; + } + if (error) { + mesg += ": "; + mesg += strerror(error); + } + return mesg; +} + +void temp_fstream::open (std::ios_base::openmode mode) +{ + close(); + + const char* tmpdir = getenv("TMPDIR"); + size_t tmpdir_len = tmpdir ? std::strlen(tmpdir) : 0; + if (tmpdir_len == 0 || tmpdir_len > 4096) { + // no $TMPDIR or it's excessively long => fall back to /tmp + tmpdir = "/tmp"; + tmpdir_len = 4; + } + std::vector path_buffer(tmpdir_len + 18); + char* path = &path_buffer[0]; + std::strcpy(path, tmpdir); + std::strcpy(path + tmpdir_len, "/git-crypt.XXXXXX"); + mode_t old_umask = umask(0077); + int fd = mkstemp(path); + if (fd == -1) { + int mkstemp_errno = errno; + umask(old_umask); + throw System_error("mkstemp", "", mkstemp_errno); + } + umask(old_umask); + std::fstream::open(path, mode); + if (!std::fstream::is_open()) { + unlink(path); + ::close(fd); + throw System_error("std::fstream::open", path, 0); + } + unlink(path); + ::close(fd); +} + +void temp_fstream::close () +{ + if (std::fstream::is_open()) { + std::fstream::close(); + } +} + +void mkdir_parent (const std::string& path) +{ + std::string::size_type slash(path.find('/', 1)); + while (slash != std::string::npos) { + std::string prefix(path.substr(0, slash)); + struct stat status; + if (stat(prefix.c_str(), &status) == 0) { + // already exists - make sure it's a directory + if (!S_ISDIR(status.st_mode)) { + throw System_error("mkdir_parent", prefix, ENOTDIR); + } + } else { + if (errno != ENOENT) { + throw System_error("mkdir_parent", prefix, errno); + } + // doesn't exist - mkdir it + if (mkdir(prefix.c_str(), 0777) == -1) { + throw System_error("mkdir", prefix, errno); + } + } + + slash = path.find('/', slash + 1); + } +} + +static std::string readlink (const char* pathname) +{ + std::vector buffer(64); + ssize_t len; + + while ((len = ::readlink(pathname, &buffer[0], buffer.size())) == static_cast(buffer.size())) { + // buffer may have been truncated - grow and try again + buffer.resize(buffer.size() * 2); + } + if (len == -1) { + throw System_error("readlink", pathname, errno); + } + + return std::string(buffer.begin(), buffer.begin() + len); +} + +std::string our_exe_path () +{ + try { + return readlink("/proc/self/exe"); + } catch (const System_error&) { + if (argv0[0] == '/') { + // argv[0] starts with / => it's an absolute path + return argv0; + } else if (std::strchr(argv0, '/')) { + // argv[0] contains / => it a relative path that should be resolved + char* resolved_path_p = realpath(argv0, NULL); + std::string resolved_path(resolved_path_p); + free(resolved_path_p); + return resolved_path; + } else { + // argv[0] is just a bare filename => not much we can do + return argv0; + } + } +} + +int exec_command (const char* 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]); + } + execl("/bin/sh", "sh", "-c", command, NULL); + perror("/bin/sh"); + _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 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; +} + +static void init_std_streams_platform () +{ +} diff --git a/util-win32.cpp b/util-win32.cpp new file mode 100644 index 0000000..d34e635 --- /dev/null +++ b/util-win32.cpp @@ -0,0 +1,124 @@ +/* + * 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 +#include +#include +#include + +std::string System_error::message () const +{ + std::string mesg(action); + if (!target.empty()) { + mesg += ": "; + mesg += target; + } + if (error) { + // TODO: use FormatMessage() + } + return mesg; +} + +void temp_fstream::open (std::ios_base::openmode mode) +{ + close(); + + char tmpdir[MAX_PATH + 1]; + + DWORD ret = GetTempPath(sizeof(tmpdir), tmpdir); + if (ret == 0) { + throw System_error("GetTempPath", "", GetLastError()); + } else if (ret > sizeof(tmpdir) - 1) { + throw System_error("GetTempPath", "", ERROR_BUFFER_OVERFLOW); + } + + char tmpfilename[MAX_PATH + 1]; + if (GetTempFileName(tmpdir, TEXT("git-crypt"), 0, tmpfilename) == 0) { + throw System_error("GetTempFileName", "", GetLastError()); + } + + filename = tmpfilename; + + std::fstream::open(filename.c_str(), mode); + if (!std::fstream::is_open()) { + DeleteFile(filename.c_str()); + throw System_error("std::fstream::open", filename, 0); + } +} + +void temp_fstream::close () +{ + if (std::fstream::is_open()) { + std::fstream::close(); + DeleteFile(filename.c_str()); + } +} + +void mkdir_parent (const std::string& path) +{ + std::string::size_type slash(path.find('/', 1)); + while (slash != std::string::npos) { + std::string prefix(path.substr(0, slash)); + if (GetFileAttributes(prefix.c_str()) == INVALID_FILE_ATTRIBUTES) { + // prefix does not exist, so try to create it + if (!CreateDirectory(prefix.c_str(), NULL)) { + throw System_error("CreateDirectory", prefix, GetLastError()); + } + } + + slash = path.find('/', slash + 1); + } +} + +std::string our_exe_path () // TODO +{ + return argv0; +} + +int exec_command (const char* command, std::ostream& output) // TODO +{ + return -1; +} + +int exec_command_with_input (const char* command, const char* p, size_t len) // TODO +{ + return -1; +} + +bool successful_exit (int status) // TODO +{ + return status == 0; +} + +static void init_std_streams_platform () +{ + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); +} diff --git a/util.cpp b/util.cpp index cd1c514..84e8253 100644 --- a/util.cpp +++ b/util.cpp @@ -31,198 +31,7 @@ #include "git-crypt.hpp" #include "util.hpp" #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -void mkdir_parent (const std::string& path) -{ - std::string::size_type slash(path.find('/', 1)); - while (slash != std::string::npos) { - std::string prefix(path.substr(0, slash)); - struct stat status; - if (stat(prefix.c_str(), &status) == 0) { - // already exists - make sure it's a directory - if (!S_ISDIR(status.st_mode)) { - throw System_error("mkdir_parent", prefix, ENOTDIR); - } - } else { - if (errno != ENOENT) { - throw System_error("mkdir_parent", prefix, errno); - } - // doesn't exist - mkdir it - if (mkdir(prefix.c_str(), 0777) == -1) { - throw System_error("mkdir", prefix, errno); - } - } - - slash = path.find('/', slash + 1); - } -} - -std::string readlink (const char* pathname) -{ - std::vector buffer(64); - ssize_t len; - - while ((len = ::readlink(pathname, &buffer[0], buffer.size())) == static_cast(buffer.size())) { - // buffer may have been truncated - grow and try again - buffer.resize(buffer.size() * 2); - } - if (len == -1) { - throw System_error("readlink", pathname, errno); - } - - return std::string(buffer.begin(), buffer.begin() + len); -} - -std::string our_exe_path () -{ - try { - return readlink("/proc/self/exe"); - } catch (const System_error&) { - if (argv0[0] == '/') { - // argv[0] starts with / => it's an absolute path - return argv0; - } else if (std::strchr(argv0, '/')) { - // argv[0] contains / => it a relative path that should be resolved - char* resolved_path_p = realpath(argv0, NULL); - std::string resolved_path(resolved_path_p); - free(resolved_path_p); - return resolved_path; - } else { - // argv[0] is just a bare filename => not much we can do - return argv0; - } - } -} - -int exec_command (const char* 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]); - } - execl("/bin/sh", "sh", "-c", command, NULL); - perror("/bin/sh"); - _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 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; -} - -void open_tempfile (std::fstream& file, std::ios_base::openmode mode) -{ - const char* tmpdir = getenv("TMPDIR"); - size_t tmpdir_len = tmpdir ? std::strlen(tmpdir) : 0; - if (tmpdir_len == 0 || tmpdir_len > 4096) { - // no $TMPDIR or it's excessively long => fall back to /tmp - tmpdir = "/tmp"; - tmpdir_len = 4; - } - std::vector path_buffer(tmpdir_len + 18); - char* path = &path_buffer[0]; - std::strcpy(path, tmpdir); - std::strcpy(path + tmpdir_len, "/git-crypt.XXXXXX"); - mode_t old_umask = umask(0077); - int fd = mkstemp(path); - if (fd == -1) { - int mkstemp_errno = errno; - umask(old_umask); - throw System_error("mkstemp", "", mkstemp_errno); - } - umask(old_umask); - file.open(path, mode); - if (!file.is_open()) { - unlink(path); - close(fd); - throw System_error("std::fstream::open", path, 0); - } - unlink(path); - close(fd); -} +#include std::string escape_shell_arg (const std::string& str) { @@ -272,3 +81,22 @@ void write_be32 (std::ostream& out, uint32_t i) out.write(reinterpret_cast(buffer), 4); } +static void init_std_streams_platform (); // platform-specific initialization + +void init_std_streams () +{ + // The following two lines are essential for achieving good performance: + std::ios_base::sync_with_stdio(false); + std::cin.tie(0); + + std::cin.exceptions(std::ios_base::badbit); + std::cout.exceptions(std::ios_base::badbit); + + init_std_streams_platform(); +} + +#ifdef _WIN32 +#include "util-win32.cpp" +#else +#include "util-unix.cpp" +#endif diff --git a/util.hpp b/util.hpp index c86e53d..2637098 100644 --- a/util.hpp +++ b/util.hpp @@ -35,6 +35,7 @@ #include #include #include +#include struct System_error { std::string action; @@ -42,20 +43,30 @@ struct System_error { int error; System_error (const std::string& a, const std::string& t, int e) : action(a), target(t), error(e) { } + + std::string message () const; +}; + +class temp_fstream : public std::fstream { + std::string filename; +public: + ~temp_fstream () { close(); } + + void open (std::ios_base::openmode); + void close (); }; void mkdir_parent (const std::string& path); // Create parent directories of path, __but not path itself__ -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&); uint32_t load_be32 (const unsigned char*); void store_be32 (unsigned char*, uint32_t); bool read_be32 (std::istream& in, uint32_t&); void write_be32 (std::ostream& out, uint32_t); +void init_std_streams (); #endif From 6e43b2a1cd1ea487fef86efe607b833bc304e7ef Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Tue, 10 Jun 2014 21:21:38 -0700 Subject: [PATCH 11/47] New exec_command() that takes command as array instead of string This abstracts away the details of argument quoting, which differs between Unix and Windows. Also replace all uses of the system() library call with exec_command(). Although system() exists on Windows, it executes the command via cmd.exe, which has ridiculous escaping rules. --- commands.cpp | 180 +++++++++++++++++++++++++++++++------------------ gpg.cpp | 56 ++++++++++----- util-unix.cpp | 41 +++++++++-- util-win32.cpp | 9 ++- util.hpp | 6 +- 5 files changed, 200 insertions(+), 92 deletions(-) diff --git a/commands.cpp b/commands.cpp index 1cc47ef..9519eb1 100644 --- a/commands.cpp +++ b/commands.cpp @@ -47,44 +47,43 @@ #include #include -static void configure_git_filters () +static void git_config (const std::string& name, const std::string& value) { - std::string git_crypt_path(our_exe_path()); + std::vector command; + command.push_back("git"); + command.push_back("config"); + command.push_back(name); + command.push_back(value); - // git config filter.git-crypt.smudge "/path/to/git-crypt smudge" - std::string command("git config filter.git-crypt.smudge "); - command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " smudge"); - - if (!successful_exit(system(command.c_str()))) { - throw Error("'git config' failed"); - } - - // git config filter.git-crypt.clean "/path/to/git-crypt clean" - command = "git config filter.git-crypt.clean "; - command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " clean"); - - if (!successful_exit(system(command.c_str()))) { - throw Error("'git config' failed"); - } - - // git config diff.git-crypt.textconv "/path/to/git-crypt diff" - command = "git config diff.git-crypt.textconv "; - command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " diff"); - - if (!successful_exit(system(command.c_str()))) { + if (!successful_exit(exec_command(command))) { throw Error("'git config' failed"); } } +static void configure_git_filters () +{ + std::string escaped_git_crypt_path(escape_shell_arg(our_exe_path())); + + git_config("filter.git-crypt.smudge", escaped_git_crypt_path + " smudge"); + git_config("filter.git-crypt.clean", escaped_git_crypt_path + " clean"); + git_config("diff.git-crypt.textconv", escaped_git_crypt_path + " diff"); +} + static std::string get_internal_key_path () { - std::stringstream output; + // git rev-parse --git-dir + std::vector command; + command.push_back("git"); + command.push_back("rev-parse"); + command.push_back("--git-dir"); - if (!successful_exit(exec_command("git rev-parse --git-dir", output))) { + std::stringstream output; + + if (!successful_exit(exec_command(command, output))) { throw Error("'git rev-parse --git-dir' failed - is this a Git repository?"); } - std::string path; + std::string path; std::getline(output, path); path += "/git-crypt/key"; return path; @@ -92,13 +91,19 @@ static std::string get_internal_key_path () static std::string get_repo_keys_path () { - std::stringstream output; + // git rev-parse --show-toplevel + std::vector command; + command.push_back("git"); + command.push_back("rev-parse"); + command.push_back("--show-toplevel"); - if (!successful_exit(exec_command("git rev-parse --show-toplevel", output))) { + std::stringstream output; + + if (!successful_exit(exec_command(command, output))) { throw Error("'git rev-parse --show-toplevel' failed - is this a Git repository?"); } - std::string path; + std::string path; std::getline(output, path); if (path.empty()) { @@ -110,6 +115,52 @@ static std::string get_repo_keys_path () return path; } +static std::string get_path_to_top () +{ + // git rev-parse --show-cdup + std::vector command; + command.push_back("git"); + command.push_back("rev-parse"); + command.push_back("--show-cdup"); + + std::stringstream output; + + if (!successful_exit(exec_command(command, output))) { + throw Error("'git rev-parse --show-cdup' failed - is this a Git repository?"); + } + + std::string path_to_top; + std::getline(output, path_to_top); + + return path_to_top; +} + +static void get_git_status (std::ostream& output) +{ + // git status -uno --porcelain + std::vector command; + command.push_back("git"); + command.push_back("status"); + command.push_back("-uno"); // don't show untracked files + command.push_back("--porcelain"); + + if (!successful_exit(exec_command(command, output))) { + throw Error("'git status' failed - is this a Git repository?"); + } +} + +static bool check_if_head_exists () +{ + // git rev-parse HEAD + std::vector command; + command.push_back("git"); + command.push_back("rev-parse"); + command.push_back("HEAD"); + + std::stringstream output; + return successful_exit(exec_command(command, output)); +} + static void load_key (Key_file& key_file, const char* legacy_path =0) { if (legacy_path) { @@ -421,20 +472,20 @@ int unlock (int argc, char** argv) return 2; } - // 0. Check to see if HEAD exists. See below why we do this. - bool head_exists = successful_exit(system("git rev-parse HEAD >/dev/null 2>/dev/null")); - - // 1. Make sure working directory is clean (ignoring untracked files) + // 0. Make sure working directory is clean (ignoring untracked files) // We do this because we run 'git checkout -f HEAD' later and we don't // want the user to lose any changes. 'git checkout -f HEAD' doesn't touch // untracked files so it's safe to ignore those. - int status; + + // Running 'git status' also serves as a check that the Git repo is accessible. + std::stringstream status_output; - status = exec_command("git status -uno --porcelain", status_output); - if (!successful_exit(status)) { - std::clog << "Error: 'git status' failed - is this a git repository?" << std::endl; - return 1; - } else if (status_output.peek() != -1 && head_exists) { + get_git_status(status_output); + + // 1. Check to see if HEAD exists. See below why we do this. + bool head_exists = check_if_head_exists(); + + if (status_output.peek() != -1 && head_exists) { // We only care that the working directory is dirty if HEAD exists. // If HEAD doesn't exist, we won't be resetting to it (see below) so // it doesn't matter that the working directory is dirty. @@ -446,11 +497,7 @@ int unlock (int argc, char** argv) // 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::stringstream cdup_output; - if (!successful_exit(exec_command("git rev-parse --show-cdup", cdup_output))) { - std::clog << "Error: 'git rev-parse --show-cdup' failed" << std::endl; - return 1; - } + std::string path_to_top(get_path_to_top()); // 3. Install the key Key_file key_file; @@ -504,17 +551,20 @@ int unlock (int argc, char** argv) // If HEAD doesn't exist (perhaps because this repo doesn't have any files yet) // just skip the checkout. if (head_exists) { - std::string path_to_top; - std::getline(cdup_output, path_to_top); - - std::string command("git checkout -f HEAD -- "); + // git checkout -f HEAD -- path/to/top + std::vector command; + command.push_back("git"); + command.push_back("checkout"); + command.push_back("-f"); + command.push_back("HEAD"); + command.push_back("--"); if (path_to_top.empty()) { - command += "."; + command.push_back("."); } else { - command += escape_shell_arg(path_to_top); + command.push_back(path_to_top); } - if (!successful_exit(system(command.c_str()))) { + if (!successful_exit(exec_command(command))) { std::clog << "Error: 'git checkout' failed" << std::endl; std::clog << "git-crypt has been set up but existing encrypted files have not been decrypted" << std::endl; return 1; @@ -563,13 +613,12 @@ int add_collab (int argc, char** argv) // 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()))) { + // git add NEW_FILE ... + std::vector command; + command.push_back("git"); + command.push_back("add"); + command.insert(command.end(), new_files.begin(), new_files.end()); + if (!successful_exit(exec_command(command))) { std::clog << "Error: 'git add' failed" << std::endl; return 1; } @@ -582,14 +631,15 @@ int add_collab (int argc, char** argv) 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); - } + // git commit -m MESSAGE NEW_FILE ... + command.clear(); + command.push_back("git"); + command.push_back("commit"); + command.push_back("-m"); + command.push_back(commit_message_builder.str()); + command.insert(command.end(), new_files.begin(), new_files.end()); - if (!successful_exit(system(command.c_str()))) { + if (!successful_exit(exec_command(command))) { std::clog << "Error: 'git commit' failed" << std::endl; return 1; } diff --git a/gpg.cpp b/gpg.cpp index 05db9fb..4813b35 100644 --- a/gpg.cpp +++ b/gpg.cpp @@ -61,10 +61,15 @@ std::string gpg_shorten_fingerprint (const std::string& fingerprint) 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::vector command; + command.push_back("gpg"); + command.push_back("--batch"); + command.push_back("--with-colons"); + command.push_back("--fixed-list-mode"); + command.push_back("--list-keys"); + command.push_back("0x" + fingerprint); std::stringstream command_output; - if (!successful_exit(exec_command(command.c_str(), command_output))) { + if (!successful_exit(exec_command(command, command_output))) { // This could happen if the keyring does not contain a public key with this fingerprint return ""; } @@ -88,10 +93,15 @@ 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::vector command; + command.push_back("gpg"); + command.push_back("--batch"); + command.push_back("--with-colons"); + command.push_back("--fingerprint"); + command.push_back("--list-keys"); + command.push_back(query); std::stringstream command_output; - if (successful_exit(exec_command(command.c_str(), command_output))) { + if (successful_exit(exec_command(command, command_output))) { while (command_output.peek() != -1) { std::string line; std::getline(command_output, line); @@ -109,8 +119,14 @@ std::vector gpg_lookup_key (const std::string& query) std::vector gpg_list_secret_keys () { // gpg --batch --with-colons --list-secret-keys --fingerprint + std::vector command; + command.push_back("gpg"); + command.push_back("--batch"); + command.push_back("--with-colons"); + command.push_back("--list-secret-keys"); + command.push_back("--fingerprint"); std::stringstream command_output; - if (!successful_exit(exec_command("gpg --batch --with-colons --list-secret-keys --fingerprint", command_output))) { + if (!successful_exit(exec_command(command, command_output))) { throw Gpg_error("gpg --list-secret-keys failed"); } @@ -132,22 +148,28 @@ 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) { // 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))) { + std::vector command; + command.push_back("gpg"); + command.push_back("--batch"); + command.push_back("-o"); + command.push_back(filename); + command.push_back("-r"); + command.push_back("0x" + recipient_fingerprint); + command.push_back("-e"); + if (!successful_exit(exec_command_with_input(command, 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))) { + // gpg -q -d FILENAME + std::vector command; + command.push_back("gpg"); + command.push_back("-q"); + command.push_back("-d"); + command.push_back(filename); + if (!successful_exit(exec_command(command, output))) { throw Gpg_error("Failed to decrypt"); } } diff --git a/util-unix.cpp b/util-unix.cpp index 214f501..7f92a58 100644 --- a/util-unix.cpp +++ b/util-unix.cpp @@ -156,7 +156,36 @@ std::string our_exe_path () } } -int exec_command (const char* command, std::ostream& output) +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])); +} + +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) { @@ -175,8 +204,8 @@ int exec_command (const char* command, std::ostream& output) dup2(pipefd[1], 1); close(pipefd[1]); } - execl("/bin/sh", "sh", "-c", command, NULL); - perror("/bin/sh"); + execvp(command[0], command); + perror(command[0].c_str()); _exit(-1); } close(pipefd[1]); @@ -198,7 +227,7 @@ 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 exec_command_with_input (const std::vector& command, const char* p, size_t len) { int pipefd[2]; if (pipe(pipefd) == -1) { @@ -217,8 +246,8 @@ int exec_command_with_input (const char* command, const char* p, size_t len) dup2(pipefd[0], 0); close(pipefd[0]); } - execl("/bin/sh", "sh", "-c", command, NULL); - perror("/bin/sh"); + execvp(command[0], command); + perror(command[0].c_str()); _exit(-1); } close(pipefd[0]); diff --git a/util-win32.cpp b/util-win32.cpp index d34e635..b758fc5 100644 --- a/util-win32.cpp +++ b/util-win32.cpp @@ -102,12 +102,17 @@ std::string our_exe_path () // TODO return argv0; } -int exec_command (const char* command, std::ostream& output) // TODO +int exec_command (const std::vector& command) // TODO { return -1; } -int exec_command_with_input (const char* command, const char* p, size_t len) // TODO +int exec_command (const std::vector& command, std::ostream& output) // TODO +{ + return -1; +} + +int exec_command_with_input (const std::vector& command, const char* p, size_t len) // TODO { return -1; } diff --git a/util.hpp b/util.hpp index 2637098..7cae193 100644 --- a/util.hpp +++ b/util.hpp @@ -36,6 +36,7 @@ #include #include #include +#include struct System_error { std::string action; @@ -58,8 +59,9 @@ public: void mkdir_parent (const std::string& path); // Create parent directories of path, __but not path itself__ 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); +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); std::string escape_shell_arg (const std::string&); uint32_t load_be32 (const unsigned char*); From dcea03f0d7f96ce2ebd748df0b9e127de1336a6a Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Thu, 12 Jun 2014 21:11:58 -0700 Subject: [PATCH 12/47] Finish implementing Windows utility functions This completes Windows support, except for the build system and documentation. --- util-win32.cpp | 218 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 208 insertions(+), 10 deletions(-) diff --git a/util-win32.cpp b/util-win32.cpp index b758fc5..e852e52 100644 --- a/util-win32.cpp +++ b/util-win32.cpp @@ -32,6 +32,7 @@ #include #include #include +#include std::string System_error::message () const { @@ -41,7 +42,17 @@ std::string System_error::message () const mesg += target; } if (error) { - // TODO: use FormatMessage() + LPTSTR error_message; + FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + error, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&error_message), + 0, + NULL); + mesg += error_message; + LocalFree(error_message); } return mesg; } @@ -97,27 +108,214 @@ void mkdir_parent (const std::string& path) } } -std::string our_exe_path () // TODO +std::string our_exe_path () { - return argv0; + std::vector buffer(128); + size_t len; + + while ((len = GetModuleFileNameA(NULL, &buffer[0], buffer.size())) == buffer.size()) { + // buffer may have been truncated - grow and try again + buffer.resize(buffer.size() * 2); + } + if (len == 0) { + throw System_error("GetModuleFileNameA", "", GetLastError()); + } + + return std::string(buffer.begin(), buffer.begin() + len); } -int exec_command (const std::vector& command) // TODO +static void escape_cmdline_argument (std::string& cmdline, const std::string& arg) { - return -1; + // 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('"'); } -int exec_command (const std::vector& command, std::ostream& output) // TODO +static std::string format_cmdline (const std::vector& command) { - return -1; + 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; } -int exec_command_with_input (const std::vector& command, const char* p, size_t len) // TODO +static int wait_for_child (HANDLE child_handle) { - return -1; + 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; } -bool successful_exit (int status) // TODO +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; } From df2b472cd95d0aba99451b991da1af8c8f9d4e59 Mon Sep 17 00:00:00 2001 From: Cyril Cleaud Date: Thu, 26 Jun 2014 22:59:17 -0700 Subject: [PATCH 13/47] Add umask and rename compatibility wrappers for Windows umask() doesn't exist on Windows and is thus a no-op. rename() only works if the destination doesn't already exist, so we must unlink before renaming. --- commands.cpp | 2 +- key.cpp | 4 ++-- util-unix.cpp | 16 +++++++++++++--- util-win32.cpp | 13 +++++++++++++ util.hpp | 3 +++ 5 files changed, 32 insertions(+), 6 deletions(-) diff --git a/commands.cpp b/commands.cpp index 9519eb1..5472766 100644 --- a/commands.cpp +++ b/commands.cpp @@ -765,7 +765,7 @@ int migrate_key (int argc, char** argv) return 1; } - if (rename(new_key_file_name.c_str(), key_file_name) == -1) { + if (util_rename(new_key_file_name.c_str(), key_file_name) == -1) { std::clog << "Error: " << key_file_name << ": " << strerror(errno) << std::endl; unlink(new_key_file_name.c_str()); return 1; diff --git a/key.cpp b/key.cpp index 508ff8d..05e059c 100644 --- a/key.cpp +++ b/key.cpp @@ -134,9 +134,9 @@ bool Key_file::load_from_file (const char* key_file_name) bool Key_file::store_to_file (const char* key_file_name) const { - mode_t old_umask = umask(0077); // make sure key file is protected (TODO: Windows compat) + mode_t old_umask = util_umask(0077); // make sure key file is protected std::ofstream key_file_out(key_file_name, std::fstream::binary); - umask(old_umask); + util_umask(old_umask); if (!key_file_out) { return false; } diff --git a/util-unix.cpp b/util-unix.cpp index 7f92a58..1224e66 100644 --- a/util-unix.cpp +++ b/util-unix.cpp @@ -69,14 +69,14 @@ void temp_fstream::open (std::ios_base::openmode mode) char* path = &path_buffer[0]; std::strcpy(path, tmpdir); std::strcpy(path + tmpdir_len, "/git-crypt.XXXXXX"); - mode_t old_umask = umask(0077); + mode_t old_umask = util_umask(0077); int fd = mkstemp(path); if (fd == -1) { int mkstemp_errno = errno; - umask(old_umask); + util_umask(old_umask); throw System_error("mkstemp", "", mkstemp_errno); } - umask(old_umask); + util_umask(old_umask); std::fstream::open(path, mode); if (!std::fstream::is_open()) { unlink(path); @@ -277,3 +277,13 @@ bool successful_exit (int status) static void init_std_streams_platform () { } + +mode_t util_umask (mode_t mode) +{ + return umask(mode); +} + +int util_rename (const char* from, const char* to) +{ + return rename(from, to); +} diff --git a/util-win32.cpp b/util-win32.cpp index e852e52..b0d20d1 100644 --- a/util-win32.cpp +++ b/util-win32.cpp @@ -325,3 +325,16 @@ static void init_std_streams_platform () _setmode(_fileno(stdin), _O_BINARY); _setmode(_fileno(stdout), _O_BINARY); } + +mode_t util_umask (mode_t mode) +{ + // Not available in Windows and function not always defined in Win32 environments + return 0; +} + +int util_rename (const char* from, const char* to) +{ + // On Windows OS, it is necessary to ensure target file doesn't exist + unlink(to); + return rename(from, to); +} diff --git a/util.hpp b/util.hpp index 7cae193..cf23771 100644 --- a/util.hpp +++ b/util.hpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include @@ -69,6 +70,8 @@ void store_be32 (unsigned char*, uint32_t); bool read_be32 (std::istream& in, uint32_t&); void write_be32 (std::ostream& out, uint32_t); void init_std_streams (); +mode_t util_umask (mode_t); +int util_rename (const char*, const char*); #endif From 188a8c15fc97cbc043b5c097205631b7da067ea5 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Wed, 18 Jun 2014 22:21:00 -0700 Subject: [PATCH 14/47] Minor pedantic changes to I/O code Don't bother checking for !in because the gcount() check is quite sufficient and having both checks was confusing. Make some variables const because they can be. --- commands.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/commands.cpp b/commands.cpp index 5472766..bc983d0 100644 --- a/commands.cpp +++ b/commands.cpp @@ -259,7 +259,7 @@ int clean (int argc, char** argv) while (std::cin && file_size < Aes_ctr_encryptor::MAX_CRYPT_BYTES) { std::cin.read(buffer, sizeof(buffer)); - size_t bytes_read = std::cin.gcount(); + const size_t bytes_read = std::cin.gcount(); hmac.add(reinterpret_cast(buffer), bytes_read); file_size += bytes_read; @@ -317,7 +317,7 @@ int clean (int argc, char** argv) const unsigned char* file_data = reinterpret_cast(file_contents.data()); size_t file_data_len = file_contents.size(); while (file_data_len > 0) { - size_t buffer_len = std::min(sizeof(buffer), file_data_len); + const size_t buffer_len = std::min(sizeof(buffer), file_data_len); aes.process(file_data, reinterpret_cast(buffer), buffer_len); std::cout.write(buffer, buffer_len); file_data += buffer_len; @@ -330,7 +330,7 @@ int clean (int argc, char** argv) while (temp_file.peek() != -1) { temp_file.read(buffer, sizeof(buffer)); - size_t buffer_len = temp_file.gcount(); + const size_t buffer_len = temp_file.gcount(); aes.process(reinterpret_cast(buffer), reinterpret_cast(buffer), @@ -359,7 +359,7 @@ int smudge (int argc, char** argv) // Read the header to get the nonce and make sure it's actually encrypted unsigned char header[10 + Aes_ctr_decryptor::NONCE_LEN]; std::cin.read(reinterpret_cast(header), sizeof(header)); - if (!std::cin || std::cin.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) { + if (std::cin.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) { std::clog << "git-crypt: error: file not encrypted" << std::endl; return 1; } @@ -403,7 +403,7 @@ int diff (int argc, char** argv) // Read the header to get the nonce and determine if it's actually encrypted unsigned char header[10 + Aes_ctr_decryptor::NONCE_LEN]; in.read(reinterpret_cast(header), sizeof(header)); - if (!in || in.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) { + if (in.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) { // File not encrypted - just copy it out to stdout std::cout.write(reinterpret_cast(header), in.gcount()); // don't forget to include the header which we read! std::cout << in.rdbuf(); From 20c0b18fa20778b79ad8ea40b19f3477a2843fd7 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 21 Jun 2014 21:16:50 -0700 Subject: [PATCH 15/47] Add a minor TODO comment --- commands.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/commands.cpp b/commands.cpp index bc983d0..b77730b 100644 --- a/commands.cpp +++ b/commands.cpp @@ -503,6 +503,7 @@ int unlock (int argc, char** argv) Key_file key_file; if (symmetric_key_file) { // Read from the symmetric key file + // TODO: command line flag to accept legacy key format? try { if (std::strcmp(symmetric_key_file, "-") == 0) { key_file.load(std::cin); From 38b43a441529ba396f47fdf4be75c0d7809396ff Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sun, 22 Jun 2014 14:18:32 -0700 Subject: [PATCH 16/47] Make 'add-collab' safe with filenames starting with '-' --- commands.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/commands.cpp b/commands.cpp index b77730b..acad590 100644 --- a/commands.cpp +++ b/commands.cpp @@ -618,6 +618,7 @@ int add_collab (int argc, char** argv) std::vector command; command.push_back("git"); command.push_back("add"); + command.push_back("--"); command.insert(command.end(), new_files.begin(), new_files.end()); if (!successful_exit(exec_command(command))) { std::clog << "Error: 'git add' failed" << std::endl; @@ -638,6 +639,7 @@ int add_collab (int argc, char** argv) command.push_back("commit"); command.push_back("-m"); command.push_back(commit_message_builder.str()); + command.push_back("--"); command.insert(command.end(), new_files.begin(), new_files.end()); if (!successful_exit(exec_command(command))) { From e6bb66b93a1ce55bf63b88fcf6355561e643dbb3 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sun, 22 Jun 2014 15:14:19 -0700 Subject: [PATCH 17/47] Add touch_file() utility function --- util-unix.cpp | 9 +++++++++ util-win32.cpp | 19 +++++++++++++++++++ util.hpp | 1 + 3 files changed, 29 insertions(+) diff --git a/util-unix.cpp b/util-unix.cpp index 1224e66..ec4ecfb 100644 --- a/util-unix.cpp +++ b/util-unix.cpp @@ -31,7 +31,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -274,6 +276,13 @@ bool successful_exit (int status) return status != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0; } +void touch_file (const std::string& filename) +{ + if (utimes(filename.c_str(), NULL) == -1) { + throw System_error("utimes", "", errno); + } +} + static void init_std_streams_platform () { } diff --git a/util-win32.cpp b/util-win32.cpp index b0d20d1..6f9d358 100644 --- a/util-win32.cpp +++ b/util-win32.cpp @@ -320,6 +320,25 @@ bool successful_exit (int status) return status == 0; } +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()); + } + SYSTEMTIME system_time; + GetSystemTime(&system_time); + FILETIME file_time; + SystemTimeToFileTime(&system_time, &file_time); + + if (!SetFileTime(fh, NULL, NULL, &file_time)) { + DWORD error = GetLastError(); + CloseHandle(fh); + throw System_error("SetFileTime", filename, error); + } + CloseHandle(fh); +} + static void init_std_streams_platform () { _setmode(_fileno(stdin), _O_BINARY); diff --git a/util.hpp b/util.hpp index cf23771..bb79ee2 100644 --- a/util.hpp +++ b/util.hpp @@ -64,6 +64,7 @@ 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&); std::string escape_shell_arg (const std::string&); uint32_t load_be32 (const unsigned char*); void store_be32 (unsigned char*, uint32_t); From f3390ff7ff18e652cf5ff487e80b51d1b74192d6 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Thu, 26 Jun 2014 19:54:11 -0700 Subject: [PATCH 18/47] Initial implementation of 'git-crypt status' 'git-crypt status' tells you which files are and aren't encrypted and detects other problems with your git-crypt setup. 'git-crypt status -f' can be used to re-stage files that were incorrectly staged unencrypted. The UI needs work, and it needs to also output the overall repository status (such as, is git-crypt even configured yet?), but this is a good start. --- commands.cpp | 323 ++++++++++++++++++++++++++++++++++++++++++++++++++ commands.hpp | 1 + git-crypt.cpp | 3 + 3 files changed, 327 insertions(+) diff --git a/commands.cpp b/commands.cpp index acad590..85a60b1 100644 --- a/commands.cpp +++ b/commands.cpp @@ -161,6 +161,106 @@ static bool check_if_head_exists () return successful_exit(exec_command(command, output)); } +// returns filter and diff attributes as a pair +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"); + command.push_back("filter"); + command.push_back("diff"); + command.push_back("--"); + command.push_back(filename); + + std::stringstream output; + if (!successful_exit(exec_command(command, output))) { + throw Error("'git check-attr' failed - is this a Git repository?"); + } + + std::string filter_attr; + std::string diff_attr; + + std::string line; + // Example output: + // filename: filter: git-crypt + // filename: diff: git-crypt + while (std::getline(output, line)) { + // filename might contain ": ", so parse line backwards + // filename: attr_name: attr_value + // ^name_pos ^value_pos + const std::string::size_type value_pos(line.rfind(": ")); + if (value_pos == std::string::npos || value_pos == 0) { + continue; + } + const std::string::size_type name_pos(line.rfind(": ", value_pos - 1)); + if (name_pos == std::string::npos) { + continue; + } + + const std::string attr_name(line.substr(name_pos + 2, value_pos - (name_pos + 2))); + const std::string attr_value(line.substr(value_pos + 2)); + + 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 + + std::vector command; + command.push_back("git"); + command.push_back("cat-file"); + command.push_back("blob"); + command.push_back(object_id); + + // TODO: do this more efficiently - don't read entire command output into buffer, only read what we need + std::stringstream output; + if (!successful_exit(exec_command(command, output))) { + throw Error("'git cat-file' failed - is this a Git repository?"); + } + + char header[10]; + output.read(header, sizeof(header)); + return output.gcount() == sizeof(header) && std::memcmp(header, "\0GITCRYPT\0", 10) == 0; +} + +static bool check_if_file_is_encrypted (const std::string& filename) +{ + // git ls-files -sz filename + std::vector command; + command.push_back("git"); + command.push_back("ls-files"); + command.push_back("-sz"); + command.push_back("--"); + command.push_back(filename); + + std::stringstream output; + if (!successful_exit(exec_command(command, output))) { + throw Error("'git ls-files' failed - is this a Git repository?"); + } + + if (output.peek() == -1) { + return false; + } + + std::string mode; + std::string object_id; + output >> mode >> object_id; + + return check_if_blob_is_encrypted(object_id); +} + static void load_key (Key_file& key_file, const char* legacy_path =0) { if (legacy_path) { @@ -788,3 +888,226 @@ int refresh (int argc, char** argv) // TODO: do a force checkout, much like in u return 1; } +int status (int argc, char** argv) +{ + int argi = 0; + + // Usage: + // git-crypt status -r [-z] Show repo status + // git-crypt status [-e | -u] [-z] [FILE ...] Show encrypted status of files + // git-crypt status -f Fix unencrypted blobs + + // Flags: + // -e show encrypted files only + // -u show unencrypted files only + // -f fix problems + // -z machine-parseable output + // -r show repo status only + + // TODO: help option / usage output + + bool repo_status_only = false; + bool show_encrypted_only = false; + bool show_unencrypted_only = false; + bool fix_problems = false; + bool machine_output = false; + + while (argi < argc && argv[argi][0] == '-') { + if (std::strcmp(argv[argi], "--") == 0) { + ++argi; + break; + } + const char* flags = argv[argi] + 1; + while (char flag = *flags++) { + switch (flag) { + case 'r': + repo_status_only = true; + break; + case 'e': + show_encrypted_only = true; + break; + case 'u': + show_unencrypted_only = true; + break; + case 'f': + fix_problems = true; + break; + case 'z': + machine_output = true; + break; + default: + std::clog << "Error: unknown option `" << flag << "'" << std::endl; + return 2; + } + } + ++argi; + } + + if (repo_status_only) { + if (show_encrypted_only || show_unencrypted_only) { + std::clog << "Error: -e and -u options cannot be used with -r" << std::endl; + return 2; + } + if (fix_problems) { + std::clog << "Error: -f option cannot be used with -r" << std::endl; + return 2; + } + if (argc - argi != 0) { + std::clog << "Error: filenames cannot be specified when -r is used" << std::endl; + return 2; + } + } + + if (show_encrypted_only && show_unencrypted_only) { + std::clog << "Error: -e and -u options are mutually exclusive" << std::endl; + return 2; + } + + if (fix_problems && (show_encrypted_only || show_unencrypted_only)) { + std::clog << "Error: -e and -u options cannot be used with -f" << std::endl; + return 2; + } + + if (machine_output) { + // TODO: implement machine-parseable output + std::clog << "Sorry, machine-parseable output is not yet implemented" << std::endl; + return 2; + } + + if (argc - argi == 0) { + // TODO: check repo status: + // is it set up for git-crypt? + // which keys are unlocked? + // --> check for filter config (see configure_git_filters()) and corresponding internal key + + if (repo_status_only) { + return 0; + } + } + + // git ls-files -cotsz --exclude-standard ... + std::vector command; + command.push_back("git"); + command.push_back("ls-files"); + command.push_back("-cotsz"); + command.push_back("--exclude-standard"); + command.push_back("--"); + if (argc - argi == 0) { + const std::string path_to_top(get_path_to_top()); + if (!path_to_top.empty()) { + command.push_back(path_to_top); + } + } else { + for (int i = argi; i < argc; ++i) { + command.push_back(argv[i]); + } + } + + std::stringstream output; + if (!successful_exit(exec_command(command, output))) { + throw Error("'git ls-files' failed - is this a Git repository?"); + } + + // Output looks like (w/o newlines): + // ? .gitignore\0 + // H 100644 06ec22e5ed0de9280731ef000a10f9c3fbc26338 0 afile\0 + + std::vector files; + bool attribute_errors = false; + bool unencrypted_blob_errors = false; + unsigned int nbr_of_fixed_blobs = 0; + unsigned int nbr_of_fix_errors = 0; + + while (output.peek() != -1) { + std::string tag; + std::string object_id; + std::string filename; + output >> tag; + if (tag != "?") { + std::string mode; + std::string stage; + output >> mode >> object_id >> stage; + } + output >> std::ws; + 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) + const std::pair file_attrs(get_file_attributes(filename)); + + if (file_attrs.first == "git-crypt") { + // File is encrypted + const bool blob_is_unencrypted = !object_id.empty() && !check_if_blob_is_encrypted(object_id); + + if (fix_problems && blob_is_unencrypted) { + if (access(filename.c_str(), F_OK) != 0) { + std::clog << "Error: " << filename << ": cannot stage encrypted version because not present in working tree - please 'git rm' or 'git checkout' it" << std::endl; + ++nbr_of_fix_errors; + } else { + touch_file(filename); + std::vector git_add_command; + git_add_command.push_back("git"); + git_add_command.push_back("add"); + git_add_command.push_back("--"); + git_add_command.push_back(filename); + if (!successful_exit(exec_command(git_add_command))) { + throw Error("'git-add' failed"); + } + if (check_if_file_is_encrypted(filename)) { + std::cout << filename << ": staged encrypted version" << std::endl; + ++nbr_of_fixed_blobs; + } else { + std::clog << "Error: " << filename << ": still unencrypted even after staging" << std::endl; + ++nbr_of_fix_errors; + } + } + } else if (!fix_problems && !show_unencrypted_only) { + std::cout << " encrypted: " << filename; + if (file_attrs.second != file_attrs.first) { + // but diff filter is not properly set + std::cout << " *** WARNING: diff=" << file_attrs.first << " attribute not set ***"; + attribute_errors = true; + } + if (blob_is_unencrypted) { + // File not actually encrypted + std::cout << " *** WARNING: staged/committed version is NOT ENCRYPTED! ***"; + unencrypted_blob_errors = true; + } + std::cout << std::endl; + } + } else { + // File not encrypted + if (!fix_problems && !show_encrypted_only) { + std::cout << "not encrypted: " << filename << std::endl; + } + } + } + + int exit_status = 0; + + if (attribute_errors) { + std::cout << std::endl; + std::cout << "Warning: one or more files has a git-crypt filter attribute but not a" << std::endl; + std::cout << "corresponding git-crypt diff attribute. For proper 'git diff' operation" << std::endl; + std::cout << "you should fix the .gitattributes file to specify the correct diff attribute." << std::endl; + std::cout << "Consult the git-crypt documentation for help." << std::endl; + exit_status = 1; + } + if (unencrypted_blob_errors) { + std::cout << std::endl; + std::cout << "Warning: one or more files is marked for encryption via .gitattributes but" << std::endl; + std::cout << "was staged and/or committed before the .gitattributes file was in effect." << std::endl; + std::cout << "Run 'git-crypt status' with the '-f' option to stage an encrypted version." << std::endl; + exit_status = 1; + } + if (nbr_of_fixed_blobs) { + std::cout << "Staged " << nbr_of_fixed_blobs << " encrypted file" << (nbr_of_fixed_blobs != 1 ? "s" : "") << "." << std::endl; + std::cout << "Warning: if these files were previously committed, unencrypted versions still exist in the repository's history." << std::endl; + } + if (nbr_of_fix_errors) { + std::cout << "Unable to stage " << nbr_of_fix_errors << " file" << (nbr_of_fix_errors != 1 ? "s" : "") << "." << std::endl; + exit_status = 1; + } + + return exit_status; +} + diff --git a/commands.hpp b/commands.hpp index 33d674b..dd2448e 100644 --- a/commands.hpp +++ b/commands.hpp @@ -53,6 +53,7 @@ int export_key (int argc, char** argv); int keygen (int argc, char** argv); int migrate_key (int argc, char** argv); int refresh (int argc, char** argv); +int status (int argc, char** argv); #endif diff --git a/git-crypt.cpp b/git-crypt.cpp index aaf27fb..b4e7261 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -159,6 +159,9 @@ try { if (std::strcmp(command, "refresh") == 0) { return refresh(argc, argv); } + if (std::strcmp(command, "status") == 0) { + return status(argc, argv); + } // Plumbing commands (executed by git, not by user): if (std::strcmp(command, "clean") == 0) { return clean(argc, argv); From bec9e7f3180ccb87f345e18d6cc1490f56057f02 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sun, 29 Jun 2014 13:49:10 -0700 Subject: [PATCH 19/47] Add parse_options helper for parsing cmd line args --- Makefile | 2 +- commands.cpp | 59 ++++++----------------- git-crypt.cpp | 4 ++ parse_options.cpp | 118 ++++++++++++++++++++++++++++++++++++++++++++++ parse_options.hpp | 60 +++++++++++++++++++++++ 5 files changed, 198 insertions(+), 45 deletions(-) create mode 100644 parse_options.cpp create mode 100644 parse_options.hpp diff --git a/Makefile b/Makefile index 2de93f9..0035245 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ CXXFLAGS := -Wall -pedantic -Wno-long-long -O2 LDFLAGS := -lcrypto PREFIX := /usr/local -OBJFILES = git-crypt.o commands.o crypto.o gpg.o key.o util.o +OBJFILES = git-crypt.o commands.o crypto.o gpg.o key.o util.o parse_options.o all: git-crypt diff --git a/commands.cpp b/commands.cpp index 85a60b1..652865a 100644 --- a/commands.cpp +++ b/commands.cpp @@ -33,6 +33,7 @@ #include "util.hpp" #include "key.hpp" #include "gpg.hpp" +#include "parse_options.hpp" #include #include #include @@ -890,58 +891,28 @@ int refresh (int argc, char** argv) // TODO: do a force checkout, much like in u int status (int argc, char** argv) { - int argi = 0; - // Usage: // git-crypt status -r [-z] Show repo status // git-crypt status [-e | -u] [-z] [FILE ...] Show encrypted status of files // git-crypt status -f Fix unencrypted blobs - // Flags: - // -e show encrypted files only - // -u show unencrypted files only - // -f fix problems - // -z machine-parseable output - // -r show repo status only - // TODO: help option / usage output - bool repo_status_only = false; - bool show_encrypted_only = false; - bool show_unencrypted_only = false; - bool fix_problems = false; - bool machine_output = false; + bool repo_status_only = false; // -r show repo status only + bool show_encrypted_only = false; // -e show encrypted files only + bool show_unencrypted_only = false; // -u show unencrypted files only + bool fix_problems = false; // -f fix problems + bool machine_output = false; // -z machine-parseable output - while (argi < argc && argv[argi][0] == '-') { - if (std::strcmp(argv[argi], "--") == 0) { - ++argi; - break; - } - const char* flags = argv[argi] + 1; - while (char flag = *flags++) { - switch (flag) { - case 'r': - repo_status_only = true; - break; - case 'e': - show_encrypted_only = true; - break; - case 'u': - show_unencrypted_only = true; - break; - case 'f': - fix_problems = true; - break; - case 'z': - machine_output = true; - break; - default: - std::clog << "Error: unknown option `" << flag << "'" << std::endl; - return 2; - } - } - ++argi; - } + Options_list options; + options.push_back(Option_def("-r", &repo_status_only)); + options.push_back(Option_def("-e", &show_encrypted_only)); + options.push_back(Option_def("-u", &show_unencrypted_only)); + options.push_back(Option_def("-f", &fix_problems)); + options.push_back(Option_def("--fix", &fix_problems)); + options.push_back(Option_def("-z", &machine_output)); + + int argi = parse_options(options, argc, argv); if (repo_status_only) { if (show_encrypted_only || show_unencrypted_only) { diff --git a/git-crypt.cpp b/git-crypt.cpp index b4e7261..d270675 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -34,6 +34,7 @@ #include "crypto.hpp" #include "key.hpp" #include "gpg.hpp" +#include "parse_options.hpp" #include #include #include @@ -188,6 +189,9 @@ try { } catch (const Crypto_error& e) { std::cerr << "git-crypt: Crypto error: " << e.where << ": " << e.message << std::endl; return 1; +} catch (const Option_error& e) { + std::cerr << "git-crypt: Error: " << e.option_name << ": " << e.message << std::endl; + return 1; } catch (Key_file::Incompatible) { std::cerr << "git-crypt: This repository contains a incompatible key file. Please upgrade git-crypt." << std::endl; return 1; diff --git a/parse_options.cpp b/parse_options.cpp new file mode 100644 index 0000000..dc93133 --- /dev/null +++ b/parse_options.cpp @@ -0,0 +1,118 @@ +/* + * 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 "parse_options.hpp" +#include + + +static const Option_def* find_option (const Options_list& options, const std::string& name) +{ + for (Options_list::const_iterator opt(options.begin()); opt != options.end(); ++opt) { + if (opt->name == name) { + return &*opt; + } + } + return 0; +} + +int parse_options (const Options_list& options, int argc, char** argv) +{ + int argi = 0; + + while (argi < argc && argv[argi][0] == '-') { + if (std::strcmp(argv[argi], "--") == 0) { + ++argi; + break; + } else if (std::strncmp(argv[argi], "--", 2) == 0) { + std::string option_name; + const char* option_value = 0; + if (char* eq = std::strchr(argv[argi], '=')) { + option_name.assign(argv[argi], eq); + option_value = eq + 1; + } else { + option_name = argv[argi]; + } + ++argi; + + const Option_def* opt(find_option(options, option_name)); + if (!opt) { + throw Option_error(option_name, "Invalid option"); + } + + if (opt->is_set) { + *opt->is_set = true; + } + if (opt->value) { + if (option_value) { + *opt->value = option_value; + } else { + if (argi >= argc) { + throw Option_error(option_name, "Option requires a value"); + } + *opt->value = argv[argi]; + ++argi; + } + } else { + if (option_value) { + throw Option_error(option_name, "Option takes no value"); + } + } + } else { + const char* arg = argv[argi] + 1; + ++argi; + while (*arg) { + std::string option_name("-"); + option_name.push_back(*arg); + ++arg; + + const Option_def* opt(find_option(options, option_name)); + if (!opt) { + throw Option_error(option_name, "Invalid option"); + } + if (opt->is_set) { + *opt->is_set = true; + } + if (opt->value) { + if (*arg) { + *opt->value = arg; + } else { + if (argi >= argc) { + throw Option_error(option_name, "Option requires a value"); + } + *opt->value = argv[argi]; + ++argi; + } + break; + } + } + } + } + return argi; +} diff --git a/parse_options.hpp b/parse_options.hpp new file mode 100644 index 0000000..d02ddaa --- /dev/null +++ b/parse_options.hpp @@ -0,0 +1,60 @@ +/* + * 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 PARSE_OPTIONS_HPP +#define PARSE_OPTIONS_HPP + +#include +#include + +struct Option_def { + std::string name; + bool* is_set; + const char** value; + + Option_def () : is_set(0), value(0) { } + Option_def (const std::string& arg_name, bool* arg_is_set) + : name(arg_name), is_set(arg_is_set), value(0) { } + Option_def (const std::string& arg_name, const char** arg_value) + : name(arg_name), is_set(0), value(arg_value) { } +}; + +typedef std::vector Options_list; + +int parse_options (const Options_list& options, int argc, char** argv); + +struct Option_error { + std::string option_name; + std::string message; + + Option_error (const std::string& n, const std::string& m) : option_name(n), message(m) { } +}; + +#endif From 1afa71183ebe4f6e9dd33e9a8a8d1fa305be3694 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sun, 29 Jun 2014 16:00:27 -0700 Subject: [PATCH 20/47] Initial implementation of multiple key support The init, export-key, add-collab, and unlock commands now take an optional -k (equivalently, --key-name) option to specify an alternative key. Files can be encrypted with the alternative key by specifying the git-crypt-KEYNAME filter in .gitattributes. Alternative key support makes it possible to encrypt different files with different keys. Note that the -k option to unlock is temporary. Unlock will eventually auto-detect the name of the key you're unlocking, either by looking in the symmetric key file, or by scanning the .git-crypt/keys directory. Note that the layout of the .git/git-crypt and .git-crypt directories has changed as follows: * .git/git-crypt/key is now .git/git-crypt/keys/default * .git-crypt/keys is now .git-crypt/keys/default --- commands.cpp | 201 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 145 insertions(+), 56 deletions(-) diff --git a/commands.cpp b/commands.cpp index 652865a..41f41ab 100644 --- a/commands.cpp +++ b/commands.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -61,16 +62,43 @@ static void git_config (const std::string& name, const std::string& value) } } -static void configure_git_filters () +static void configure_git_filters (const char* key_name) { std::string escaped_git_crypt_path(escape_shell_arg(our_exe_path())); - git_config("filter.git-crypt.smudge", escaped_git_crypt_path + " smudge"); - git_config("filter.git-crypt.clean", escaped_git_crypt_path + " clean"); - git_config("diff.git-crypt.textconv", escaped_git_crypt_path + " diff"); + if (key_name) { + // Note: key_name contains only shell-safe characters so it need not be escaped. + git_config(std::string("filter.git-crypt-") + key_name + ".smudge", + escaped_git_crypt_path + " smudge --key-name=" + key_name); + git_config(std::string("filter.git-crypt-") + key_name + ".clean", + escaped_git_crypt_path + " clean --key-name=" + key_name); + git_config(std::string("diff.git-crypt-") + key_name + ".textconv", + escaped_git_crypt_path + " diff --key-name=" + key_name); + } else { + git_config("filter.git-crypt.smudge", escaped_git_crypt_path + " smudge"); + git_config("filter.git-crypt.clean", escaped_git_crypt_path + " clean"); + git_config("diff.git-crypt.textconv", escaped_git_crypt_path + " diff"); + } } -static std::string get_internal_key_path () +static void validate_key_name (const char* key_name) +{ + if (!*key_name) { + throw Error("Key name may not be empty"); + } + + if (std::strcmp(key_name, "default") == 0) { + throw Error("`default' is not a legal key name"); + } + // Need to be restrictive with key names because they're used as part of a Git filter name + while (char c = *key_name++) { + if (!std::isalnum(c) && c != '-' && c != '_') { + throw Error("Key names may contain only A-Z, a-z, 0-9, '-', and '_'"); + } + } +} + +static std::string get_internal_key_path (const char* key_name) { // git rev-parse --git-dir std::vector command; @@ -86,7 +114,8 @@ static std::string get_internal_key_path () std::string path; std::getline(output, path); - path += "/git-crypt/key"; + path += "/git-crypt/keys/"; + path += key_name ? key_name : "default"; return path; } @@ -262,7 +291,7 @@ static bool check_if_file_is_encrypted (const std::string& filename) return check_if_blob_is_encrypted(object_id); } -static void load_key (Key_file& key_file, const char* legacy_path =0) +static void load_key (Key_file& key_file, const char* key_name, const char* key_path =0, const char* legacy_path =0) { if (legacy_path) { std::ifstream key_file_in(legacy_path, std::fstream::binary); @@ -270,20 +299,27 @@ static void load_key (Key_file& key_file, const char* legacy_path =0) throw Error(std::string("Unable to open key file: ") + legacy_path); } key_file.load_legacy(key_file_in); - } else { - std::ifstream key_file_in(get_internal_key_path().c_str(), std::fstream::binary); + } else if (key_path) { + std::ifstream key_file_in(key_path, std::fstream::binary); if (!key_file_in) { + throw Error(std::string("Unable to open key file: ") + key_path); + } + key_file.load(key_file_in); + } else { + std::ifstream key_file_in(get_internal_key_path(key_name).c_str(), std::fstream::binary); + if (!key_file_in) { + // TODO: include key name in error message throw Error("Unable to open key file - have you unlocked/initialized this repository yet?"); } key_file.load(key_file_in); } } -static bool decrypt_repo_key (Key_file& key_file, uint32_t key_version, const std::vector& secret_keys, const std::string& keys_path) +static bool decrypt_repo_key (Key_file& key_file, const char* key_name, 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; + path_builder << keys_path << '/' << (key_name ? key_name : "default") << '/' << key_version << '/' << *seckey; std::string path(path_builder.str()); if (access(path.c_str(), F_OK) == 0) { std::stringstream decrypted_contents; @@ -301,7 +337,7 @@ static bool decrypt_repo_key (Key_file& key_file, uint32_t key_version, const st 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) +static void encrypt_repo_key (const char* key_name, 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; { @@ -312,7 +348,7 @@ static void encrypt_repo_key (uint32_t key_version, const Key_file::Entry& key, 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; + path_builder << keys_path << '/' << (key_name ? key_name : "default") << '/' << key_version << '/' << *collab; std::string path(path_builder.str()); if (access(path.c_str(), F_OK) == 0) { @@ -325,21 +361,35 @@ static void encrypt_repo_key (uint32_t key_version, const Key_file::Entry& key, } } +static int parse_plumbing_options (const char** key_name, const char** key_file, int argc, char** argv) +{ + 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("--key-file", key_file)); + + return parse_options(options, argc, argv); +} + // Encrypt contents of stdin and write to stdout int clean (int argc, char** argv) { - const char* legacy_key_path = 0; - if (argc == 0) { - } else if (argc == 1) { - legacy_key_path = argv[0]; + const char* key_name = 0; + const char* key_path = 0; + const char* legacy_key_path = 0; + + int argi = parse_plumbing_options(&key_name, &key_path, argc, argv); + if (argc - argi == 0) { + } else if (!key_name && !key_path && argc - argi == 1) { // Deprecated - for compatibility with pre-0.4 + legacy_key_path = argv[argi]; } else { - std::clog << "Usage: git-crypt smudge" << std::endl; + std::clog << "Usage: git-crypt clean [--key-name=NAME] [--key-file=PATH]" << std::endl; return 2; } Key_file key_file; - load_key(key_file, legacy_key_path); + load_key(key_file, key_name, key_path, legacy_key_path); const Key_file::Entry* key = key_file.get_latest(); if (!key) { @@ -446,16 +496,20 @@ int clean (int argc, char** argv) // Decrypt contents of stdin and write to stdout int smudge (int argc, char** argv) { - const char* legacy_key_path = 0; - if (argc == 0) { - } else if (argc == 1) { - legacy_key_path = argv[0]; + const char* key_name = 0; + const char* key_path = 0; + const char* legacy_key_path = 0; + + int argi = parse_plumbing_options(&key_name, &key_path, argc, argv); + if (argc - argi == 0) { + } else if (!key_name && !key_path && argc - argi == 1) { // Deprecated - for compatibility with pre-0.4 + legacy_key_path = argv[argi]; } else { - std::clog << "Usage: git-crypt smudge" << std::endl; + std::clog << "Usage: git-crypt smudge [--key-name=NAME] [--key-file=PATH]" << std::endl; return 2; } Key_file key_file; - load_key(key_file, legacy_key_path); + load_key(key_file, key_name, key_path, legacy_key_path); // Read the header to get the nonce and make sure it's actually encrypted unsigned char header[10 + Aes_ctr_decryptor::NONCE_LEN]; @@ -479,19 +533,23 @@ int smudge (int argc, char** argv) int diff (int argc, char** argv) { - const char* filename = 0; - const char* legacy_key_path = 0; - if (argc == 1) { - filename = argv[0]; - } else if (argc == 2) { - legacy_key_path = argv[0]; - filename = argv[1]; + const char* key_name = 0; + const char* key_path = 0; + const char* filename = 0; + const char* legacy_key_path = 0; + + int argi = parse_plumbing_options(&key_name, &key_path, argc, argv); + if (argc - argi == 1) { + filename = argv[argi]; + } else if (!key_name && !key_path && argc - argi == 2) { // Deprecated - for compatibility with pre-0.4 + legacy_key_path = argv[argi]; + filename = argv[argi + 1]; } else { - std::clog << "Usage: git-crypt diff FILENAME" << std::endl; + std::clog << "Usage: git-crypt diff [--key-name=NAME] [--key-file=PATH] FILENAME" << std::endl; return 2; } Key_file key_file; - load_key(key_file, legacy_key_path); + load_key(key_file, key_name, key_path, legacy_key_path); // Open the file std::ifstream in(filename, std::fstream::binary); @@ -527,20 +585,32 @@ int diff (int argc, char** argv) int init (int argc, char** argv) { - if (argc == 1) { + const char* key_name = 0; + Options_list options; + options.push_back(Option_def("-k", &key_name)); + options.push_back(Option_def("--key-name", &key_name)); + + int argi = parse_options(options, argc, argv); + + if (!key_name && argc - argi == 1) { std::clog << "Warning: 'git-crypt init' with a key file is deprecated as of git-crypt 0.4" << std::endl; std::clog << "and will be removed in a future release. Please get in the habit of using" << std::endl; std::clog << "'git-crypt unlock KEYFILE' instead." << std::endl; return unlock(argc, argv); } - if (argc != 0) { - std::clog << "Error: 'git-crypt init' takes no arguments." << std::endl; + if (argc - argi != 0) { + std::clog << "Usage: git-crypt init [-k KEYNAME]" << std::endl; return 2; } - std::string internal_key_path(get_internal_key_path()); + if (key_name) { + validate_key_name(key_name); + } + + std::string internal_key_path(get_internal_key_path(key_name)); if (access(internal_key_path.c_str(), F_OK) == 0) { // TODO: add a -f option to reinitialize the repo anyways (this should probably imply a refresh) + // TODO: include key_name in error message std::clog << "Error: this repository has already been initialized with git-crypt." << std::endl; return 1; } @@ -557,7 +627,7 @@ int init (int argc, char** argv) } // 2. Configure git for git-crypt - configure_git_filters(); + configure_git_filters(key_name); return 0; } @@ -565,11 +635,17 @@ int init (int argc, char** argv) int unlock (int argc, char** argv) { const char* symmetric_key_file = 0; - if (argc == 0) { - } else if (argc == 1) { - symmetric_key_file = argv[0]; + const char* key_name = 0; + Options_list options; + options.push_back(Option_def("-k", &key_name)); + options.push_back(Option_def("--key-name", &key_name)); + + int argi = parse_options(options, argc, argv); + if (argc - argi == 0) { + } else if (argc - argi == 1) { + symmetric_key_file = argv[argi]; } else { - std::clog << "Usage: git-crypt unlock [KEYFILE]" << std::endl; + std::clog << "Usage: git-crypt unlock [-k KEYNAME] [KEYFILE]" << std::endl; return 2; } @@ -630,14 +706,14 @@ int unlock (int argc, char** argv) 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)) { + if (!decrypt_repo_key(key_file, key_name, 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()); + std::string internal_key_path(get_internal_key_path(key_name)); // TODO: croak if internal_key_path already exists??? mkdir_parent(internal_key_path); if (!key_file.store_to_file(internal_key_path.c_str())) { @@ -646,7 +722,7 @@ int unlock (int argc, char** argv) } // 4. Configure git for git-crypt - configure_git_filters(); + configure_git_filters(key_name); // 5. Do a force checkout so any files that were previously checked out encrypted // will now be checked out decrypted. @@ -678,15 +754,21 @@ int unlock (int argc, char** argv) int add_collab (int argc, char** argv) { - if (argc == 0) { - std::clog << "Usage: git-crypt add-collab GPG_USER_ID [...]" << std::endl; + const char* key_name = 0; + Options_list options; + options.push_back(Option_def("-k", &key_name)); + options.push_back(Option_def("--key-name", &key_name)); + + int argi = parse_options(options, argc, argv); + if (argc - argi == 0) { + std::clog << "Usage: git-crypt add-collab [-k KEYNAME] 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) { + for (int i = argi; 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; @@ -701,7 +783,7 @@ int add_collab (int argc, char** argv) // 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); + load_key(key_file, key_name); const Key_file::Entry* key = key_file.get_latest(); if (!key) { std::clog << "Error: key file is empty" << std::endl; @@ -711,7 +793,7 @@ int add_collab (int argc, char** argv) 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); + encrypt_repo_key(key_name, key_file.latest(), *key, collab_keys, keys_path, &new_files); // add/commit the new files if (!new_files.empty()) { @@ -728,6 +810,7 @@ int add_collab (int argc, char** argv) // git commit ... // TODO: add a command line option (-n perhaps) to inhibit committing + // 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) { @@ -781,16 +864,22 @@ int ls_collabs (int argc, char** argv) // TODO int export_key (int argc, char** argv) { // TODO: provide options to export only certain key versions + const char* key_name = 0; + Options_list options; + options.push_back(Option_def("-k", &key_name)); + options.push_back(Option_def("--key-name", &key_name)); - if (argc != 1) { - std::clog << "Usage: git-crypt export-key FILENAME" << std::endl; + int argi = parse_options(options, argc, argv); + + if (argc - argi != 1) { + std::clog << "Usage: git-crypt export-key [-k KEYNAME] FILENAME" << std::endl; return 2; } Key_file key_file; - load_key(key_file); + load_key(key_file, key_name); - const char* out_file_name = argv[0]; + const char* out_file_name = argv[argi]; if (std::strcmp(out_file_name, "-") == 0) { key_file.store(std::cout); @@ -1005,7 +1094,7 @@ int status (int argc, char** argv) // 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) const std::pair file_attrs(get_file_attributes(filename)); - if (file_attrs.first == "git-crypt") { + if (file_attrs.first == "git-crypt") { // TODO: key_name support // File is encrypted const bool blob_is_unencrypted = !object_id.empty() && !check_if_blob_is_encrypted(object_id); From 3c8273cd4b7eccf52719cee3e0df871382be4ee3 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sun, 29 Jun 2014 16:14:16 -0700 Subject: [PATCH 21/47] Add .gpg filename extension to in-repo encrypted keys This will help distinguish keys encrypted with GPG from keys encrypted by other means. (For example, a future version of git-crypt might support passphrase-encrypted keys.) --- commands.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commands.cpp b/commands.cpp index 41f41ab..1b99435 100644 --- a/commands.cpp +++ b/commands.cpp @@ -319,7 +319,7 @@ static bool decrypt_repo_key (Key_file& key_file, const char* key_name, uint32_t { for (std::vector::const_iterator seckey(secret_keys.begin()); seckey != secret_keys.end(); ++seckey) { std::ostringstream path_builder; - path_builder << keys_path << '/' << (key_name ? key_name : "default") << '/' << key_version << '/' << *seckey; + path_builder << keys_path << '/' << (key_name ? key_name : "default") << '/' << key_version << '/' << *seckey << ".gpg"; std::string path(path_builder.str()); if (access(path.c_str(), F_OK) == 0) { std::stringstream decrypted_contents; @@ -348,7 +348,7 @@ static void encrypt_repo_key (const char* key_name, uint32_t key_version, const for (std::vector::const_iterator collab(collab_keys.begin()); collab != collab_keys.end(); ++collab) { std::ostringstream path_builder; - path_builder << keys_path << '/' << (key_name ? key_name : "default") << '/' << key_version << '/' << *collab; + path_builder << keys_path << '/' << (key_name ? key_name : "default") << '/' << key_version << '/' << *collab << ".gpg"; std::string path(path_builder.str()); if (access(path.c_str(), F_OK) == 0) { From 3511033f7fa4dbdae921e83faee57a54dd5853d4 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sun, 29 Jun 2014 21:54:28 -0700 Subject: [PATCH 22/47] Make key files extensible, store key name in key file Storing the key name in the key file makes it unnecessary to pass the --key-name option to git-crypt unlock. This breaks compatibility with post-revamp keys. On the plus side, keys are now extensible so in the future it will be easier to make changes to the format without breaking compatibility. --- commands.cpp | 41 +++++++------ key.cpp | 166 ++++++++++++++++++++++++++++++++++++++++++++++++--- key.hpp | 32 +++++++++- 3 files changed, 206 insertions(+), 33 deletions(-) diff --git a/commands.cpp b/commands.cpp index 1b99435..d534276 100644 --- a/commands.cpp +++ b/commands.cpp @@ -81,20 +81,11 @@ static void configure_git_filters (const char* key_name) } } -static void validate_key_name (const char* key_name) +static void validate_key_name_or_throw (const char* key_name) { - if (!*key_name) { - throw Error("Key name may not be empty"); - } - - if (std::strcmp(key_name, "default") == 0) { - throw Error("`default' is not a legal key name"); - } - // Need to be restrictive with key names because they're used as part of a Git filter name - while (char c = *key_name++) { - if (!std::isalnum(c) && c != '-' && c != '_') { - throw Error("Key names may contain only A-Z, a-z, 0-9, '-', and '_'"); - } + std::string reason; + if (!validate_key_name(key_name, &reason)) { + throw Error(reason); } } @@ -330,25 +321,26 @@ static bool decrypt_repo_key (Key_file& key_file, const char* key_name, uint32_t 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); + key_file.add(*this_version_entry); return true; } } return false; } -static void encrypt_repo_key (const char* key_name, uint32_t key_version, 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; { Key_file this_version_key_file; - this_version_key_file.add(key_version, key); + this_version_key_file.set_key_name(key_name); + this_version_key_file.add(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_name ? key_name : "default") << '/' << key_version << '/' << *collab << ".gpg"; + path_builder << keys_path << '/' << (key_name ? key_name : "default") << '/' << key.version << '/' << *collab << ".gpg"; std::string path(path_builder.str()); if (access(path.c_str(), F_OK) == 0) { @@ -604,7 +596,7 @@ int init (int argc, char** argv) } if (key_name) { - validate_key_name(key_name); + validate_key_name_or_throw(key_name); } std::string internal_key_path(get_internal_key_path(key_name)); @@ -618,6 +610,7 @@ int init (int argc, char** argv) // 1. Generate a key and install it std::clog << "Generating key..." << std::endl; Key_file key_file; + key_file.set_key_name(key_name); key_file.generate(); mkdir_parent(internal_key_path); @@ -681,6 +674,12 @@ int unlock (int argc, char** argv) if (symmetric_key_file) { // Read from the symmetric key file // TODO: command line flag to accept legacy key format? + + if (key_name) { + std::clog << "Error: key name should not be specified when unlocking with symmetric key." << std::endl; + return 1; + } + try { if (std::strcmp(symmetric_key_file, "-") == 0) { key_file.load(std::cin); @@ -713,7 +712,7 @@ int unlock (int argc, char** argv) return 1; } } - std::string internal_key_path(get_internal_key_path(key_name)); + std::string internal_key_path(get_internal_key_path(key_file.get_key_name())); // TODO: croak if internal_key_path already exists??? mkdir_parent(internal_key_path); if (!key_file.store_to_file(internal_key_path.c_str())) { @@ -722,7 +721,7 @@ int unlock (int argc, char** argv) } // 4. Configure git for git-crypt - configure_git_filters(key_name); + configure_git_filters(key_file.get_key_name()); // 5. Do a force checkout so any files that were previously checked out encrypted // will now be checked out decrypted. @@ -793,7 +792,7 @@ int add_collab (int argc, char** argv) std::string keys_path(get_repo_keys_path()); std::vector new_files; - encrypt_repo_key(key_name, key_file.latest(), *key, collab_keys, keys_path, &new_files); + encrypt_repo_key(key_name, *key, collab_keys, keys_path, &new_files); // add/commit the new files if (!new_files.empty()) { diff --git a/key.cpp b/key.cpp index 05e059c..80d22af 100644 --- a/key.cpp +++ b/key.cpp @@ -40,9 +40,69 @@ #include #include #include +#include + +Key_file::Entry::Entry () +{ + version = 0; + std::memset(aes_key, 0, AES_KEY_LEN); + std::memset(hmac_key, 0, HMAC_KEY_LEN); +} void Key_file::Entry::load (std::istream& in) { + while (true) { + uint32_t field_id; + if (!read_be32(in, field_id)) { + throw Malformed(); + } + if (field_id == KEY_FIELD_END) { + break; + } + uint32_t field_len; + if (!read_be32(in, field_len)) { + throw Malformed(); + } + + if (field_id == KEY_FIELD_VERSION) { + if (field_len != 4) { + throw Malformed(); + } + if (!read_be32(in, version)) { + throw Malformed(); + } + } else if (field_id == KEY_FIELD_AES_KEY) { + if (field_len != AES_KEY_LEN) { + throw Malformed(); + } + in.read(reinterpret_cast(aes_key), AES_KEY_LEN); + if (in.gcount() != AES_KEY_LEN) { + throw Malformed(); + } + } else if (field_id == KEY_FIELD_HMAC_KEY) { + if (field_len != HMAC_KEY_LEN) { + throw Malformed(); + } + in.read(reinterpret_cast(hmac_key), HMAC_KEY_LEN); + if (in.gcount() != HMAC_KEY_LEN) { + throw Malformed(); + } + } else if (field_id & 1) { // unknown critical field + throw Incompatible(); + } else { + // unknown non-critical field - safe to ignore + in.ignore(field_len); + if (in.gcount() != field_len) { + throw Malformed(); + } + } + } +} + +void Key_file::Entry::load_legacy (uint32_t arg_version, std::istream& in) +{ + version = arg_version; + // First comes the AES key in.read(reinterpret_cast(aes_key), AES_KEY_LEN); if (in.gcount() != AES_KEY_LEN) { @@ -58,12 +118,28 @@ void Key_file::Entry::load (std::istream& in) void Key_file::Entry::store (std::ostream& out) const { + // Version + write_be32(out, KEY_FIELD_VERSION); + write_be32(out, 4); + write_be32(out, version); + + // AES key + write_be32(out, KEY_FIELD_AES_KEY); + write_be32(out, AES_KEY_LEN); out.write(reinterpret_cast(aes_key), AES_KEY_LEN); + + // HMAC key + write_be32(out, KEY_FIELD_HMAC_KEY); + write_be32(out, HMAC_KEY_LEN); out.write(reinterpret_cast(hmac_key), HMAC_KEY_LEN); + + // End + write_be32(out, KEY_FIELD_END); } -void Key_file::Entry::generate () +void Key_file::Entry::generate (uint32_t arg_version) { + version = arg_version; random_bytes(aes_key, AES_KEY_LEN); random_bytes(hmac_key, HMAC_KEY_LEN); } @@ -79,15 +155,15 @@ const Key_file::Entry* Key_file::get (uint32_t version) const return it != entries.end() ? &it->second : 0; } -void Key_file::add (uint32_t version, const Entry& entry) +void Key_file::add (const Entry& entry) { - entries[version] = entry; + entries[entry.version] = entry; } void Key_file::load_legacy (std::istream& in) { - entries[0].load(in); + entries[0].load_legacy(0, in); } void Key_file::load (std::istream& in) @@ -103,12 +179,52 @@ void Key_file::load (std::istream& in) if (load_be32(preamble + 12) != FORMAT_VERSION) { throw Incompatible(); } + load_header(in); while (in.peek() != -1) { - uint32_t version; - if (!read_be32(in, version)) { + Entry entry; + entry.load(in); + add(entry); + } +} + +void Key_file::load_header (std::istream& in) +{ + while (true) { + uint32_t field_id; + if (!read_be32(in, field_id)) { throw Malformed(); } - entries[version].load(in); + if (field_id == HEADER_FIELD_END) { + break; + } + uint32_t field_len; + if (!read_be32(in, field_len)) { + throw Malformed(); + } + + if (field_id == HEADER_FIELD_KEY_NAME) { + if (field_len > KEY_NAME_MAX_LEN) { + throw Malformed(); + } + std::vector bytes(field_len); + in.read(&bytes[0], field_len); + if (in.gcount() != field_len) { + throw Malformed(); + } + key_name.assign(&bytes[0], field_len); + if (!validate_key_name(key_name.c_str())) { + key_name.clear(); + throw Malformed(); + } + } else if (field_id & 1) { // unknown critical field + throw Incompatible(); + } else { + // unknown non-critical field - safe to ignore + in.ignore(field_len); + if (in.gcount() != field_len) { + throw Malformed(); + } + } } } @@ -116,8 +232,13 @@ void Key_file::store (std::ostream& out) const { out.write("\0GITCRYPTKEY", 12); write_be32(out, FORMAT_VERSION); + if (!key_name.empty()) { + write_be32(out, HEADER_FIELD_KEY_NAME); + write_be32(out, key_name.size()); + out.write(key_name.data(), key_name.size()); + } + write_be32(out, HEADER_FIELD_END); for (Map::const_iterator it(entries.begin()); it != entries.end(); ++it) { - write_be32(out, it->first); it->second.store(out); } } @@ -157,7 +278,8 @@ std::string Key_file::store_to_string () const void Key_file::generate () { - entries[is_empty() ? 0 : latest() + 1].generate(); + uint32_t version(is_empty() ? 0 : latest() + 1); + entries[version].generate(version); } uint32_t Key_file::latest () const @@ -168,3 +290,29 @@ uint32_t Key_file::latest () const return entries.begin()->first; } +bool validate_key_name (const char* key_name, std::string* reason) +{ + if (!*key_name) { + if (reason) { *reason = "Key name may not be empty"; } + return false; + } + + if (std::strcmp(key_name, "default") == 0) { + if (reason) { *reason = "`default' is not a legal key name"; } + return false; + } + // Need to be restrictive with key names because they're used as part of a Git filter name + size_t len = 0; + while (char c = *key_name++) { + if (!std::isalnum(c) && c != '-' && c != '_') { + if (reason) { *reason = "Key names may contain only A-Z, a-z, 0-9, '-', and '_'"; } + return false; + } + if (++len > KEY_NAME_MAX_LEN) { + if (reason) { *reason = "Key name is too long"; } + return false; + } + } + return true; +} + diff --git a/key.hpp b/key.hpp index 30a4216..ac4f9af 100644 --- a/key.hpp +++ b/key.hpp @@ -45,12 +45,16 @@ enum { struct Key_file { public: struct Entry { + uint32_t version; unsigned char aes_key[AES_KEY_LEN]; unsigned char hmac_key[HMAC_KEY_LEN]; + Entry (); + void load (std::istream&); + void load_legacy (uint32_t version, std::istream&); void store (std::ostream&) const; - void generate (); + void generate (uint32_t version); }; struct Malformed { }; // exception class @@ -59,7 +63,7 @@ public: const Entry* get_latest () const; const Entry* get (uint32_t version) const; - void add (uint32_t version, const Entry&); + void add (const Entry&); void load_legacy (std::istream&); void load (std::istream&); @@ -77,11 +81,33 @@ public: uint32_t latest () const; + void set_key_name (const char* k) { key_name = k ? k : ""; } + const char* get_key_name () const { return key_name.empty() ? 0 : key_name.c_str(); } private: typedef std::map > Map; - enum { FORMAT_VERSION = 1 }; + enum { FORMAT_VERSION = 2 }; Map entries; + std::string key_name; + + void load_header (std::istream&); + + enum { + HEADER_FIELD_END = 0, + HEADER_FIELD_KEY_NAME = 1 + }; + enum { + KEY_FIELD_END = 0, + KEY_FIELD_VERSION = 1, + KEY_FIELD_AES_KEY = 3, + KEY_FIELD_HMAC_KEY = 5 + }; }; +enum { + KEY_NAME_MAX_LEN = 128 +}; + +bool validate_key_name (const char* key_name, std::string* reason =0); + #endif From 4af0a0cfc1a46582fecf4ff2ce746408213cec58 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Wed, 2 Jul 2014 22:08:45 -0700 Subject: [PATCH 23/47] Avoid unsafe integer signedness conversions when loading key file --- key.cpp | 12 +++++++++--- key.hpp | 3 +++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/key.cpp b/key.cpp index 80d22af..552ae38 100644 --- a/key.cpp +++ b/key.cpp @@ -91,8 +91,11 @@ void Key_file::Entry::load (std::istream& in) throw Incompatible(); } else { // unknown non-critical field - safe to ignore + if (field_len > MAX_FIELD_LEN) { + throw Malformed(); + } in.ignore(field_len); - if (in.gcount() != field_len) { + if (in.gcount() != static_cast(field_len)) { throw Malformed(); } } @@ -208,7 +211,7 @@ void Key_file::load_header (std::istream& in) } std::vector bytes(field_len); in.read(&bytes[0], field_len); - if (in.gcount() != field_len) { + if (in.gcount() != static_cast(field_len)) { throw Malformed(); } key_name.assign(&bytes[0], field_len); @@ -220,8 +223,11 @@ void Key_file::load_header (std::istream& in) throw Incompatible(); } else { // unknown non-critical field - safe to ignore + if (field_len > MAX_FIELD_LEN) { + throw Malformed(); + } in.ignore(field_len); - if (in.gcount() != field_len) { + if (in.gcount() != static_cast(field_len)) { throw Malformed(); } } diff --git a/key.hpp b/key.hpp index ac4f9af..2695581 100644 --- a/key.hpp +++ b/key.hpp @@ -102,6 +102,9 @@ private: KEY_FIELD_AES_KEY = 3, KEY_FIELD_HMAC_KEY = 5 }; + enum { + MAX_FIELD_LEN = 1<<20 + }; }; enum { From f03d972937dbc19787ff8d1e70e26c8877f396da Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Wed, 2 Jul 2014 22:10:09 -0700 Subject: [PATCH 24/47] Add get_directory_contents utility function --- util-unix.cpp | 24 ++++++++++++++++++++++++ util-win32.cpp | 29 +++++++++++++++++++++++++++++ util.hpp | 1 + 3 files changed, 54 insertions(+) diff --git a/util-unix.cpp b/util-unix.cpp index ec4ecfb..2385566 100644 --- a/util-unix.cpp +++ b/util-unix.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -296,3 +297,26 @@ int util_rename (const char* from, const char* to) { return rename(from, to); } + +static int dirfilter (const struct dirent* ent) +{ + // filter out . and .. + return std::strcmp(ent->d_name, ".") != 0 && std::strcmp(ent->d_name, "..") != 0; +} + +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); + + return contents; +} diff --git a/util-win32.cpp b/util-win32.cpp index 6f9d358..4e6e9c1 100644 --- a/util-win32.cpp +++ b/util-win32.cpp @@ -33,6 +33,7 @@ #include #include #include +#include std::string System_error::message () const { @@ -357,3 +358,31 @@ int util_rename (const char* from, const char* to) unlink(to); return rename(from, to); } + +std::vector get_directory_contents (const char* path) +{ + std::vector filenames; + std::string patt(path); + if (!patt.empty() && patt[patt.size() - 1] != '/' && patt[patt.size() - 1] != '\\') { + patt.push_back('\\'); + } + patt.push_back('*'); + + WIN32_FIND_DATAA ffd; + HANDLE h = FindFirstFileA(patt.c_str(), &ffd); + if (h == INVALID_HANDLE_VALUE) { + throw System_error("FindFirstFileA", patt, GetLastError()); + } + do { + if (std::strcmp(ffd.cFileName, ".") != 0 && std::strcmp(ffd.cFileName, "..") != 0) { + filenames.push_back(ffd.cFileName); + } + } while (FindNextFileA(h, &ffd) != 0); + + DWORD err = GetLastError(); + if (err != ERROR_NO_MORE_FILES) { + throw System_error("FileNextFileA", patt, err); + } + FindClose(h); + return filenames; +} diff --git a/util.hpp b/util.hpp index bb79ee2..107cdfc 100644 --- a/util.hpp +++ b/util.hpp @@ -73,6 +73,7 @@ void write_be32 (std::ostream& out, uint32_t); void init_std_streams (); mode_t util_umask (mode_t); int util_rename (const char*, const char*); +std::vector get_directory_contents (const char* path); #endif From 2ba7f0e3749e3a37c262a8156f13cd42dde8dbc1 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Wed, 2 Jul 2014 22:12:13 -0700 Subject: [PATCH 25/47] unlock: decrypt all possible keys when using GPG It's no longer necessary to specify the -k option to unlock when using GPG. unlock will automatically decrypt all keys which the user can access. --- commands.cpp | 127 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 78 insertions(+), 49 deletions(-) diff --git a/commands.cpp b/commands.cpp index d534276..87d222d 100644 --- a/commands.cpp +++ b/commands.cpp @@ -81,6 +81,11 @@ static void configure_git_filters (const char* key_name) } } +static bool same_key_name (const char* a, const char* b) +{ + return (!a && !b) || (a && b && std::strcmp(a, b) == 0); +} + static void validate_key_name_or_throw (const char* key_name) { std::string reason; @@ -321,6 +326,10 @@ static bool decrypt_repo_key (Key_file& key_file, const char* key_name, uint32_t if (!this_version_entry) { throw Error("GPG-encrypted keyfile is malformed because it does not contain expected key version"); } + if (!same_key_name(key_name, this_version_key_file.get_key_name())) { + throw Error("GPG-encrypted keyfile is malformed because it does not contain expected key name"); + } + key_file.set_key_name(key_name); key_file.add(*this_version_entry); return true; } @@ -328,6 +337,33 @@ static bool decrypt_repo_key (Key_file& key_file, const char* key_name, uint32_t return false; } +static bool decrypt_repo_keys (std::vector& key_files, uint32_t key_version, const std::vector& secret_keys, const std::string& keys_path) +{ + bool successful = false; + std::vector dirents; + + if (access(keys_path.c_str(), F_OK) == 0) { + dirents = get_directory_contents(keys_path.c_str()); + } + + for (std::vector::const_iterator dirent(dirents.begin()); dirent != dirents.end(); ++dirent) { + const char* key_name = 0; + if (*dirent != "default") { + if (!validate_key_name(dirent->c_str())) { + continue; + } + key_name = dirent->c_str(); + } + + Key_file key_file; + if (decrypt_repo_key(key_file, key_name, key_version, secret_keys, keys_path)) { + key_files.push_back(key_file); + successful = true; + } + } + 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) { std::string key_file_data; @@ -627,21 +663,6 @@ int init (int argc, char** argv) int unlock (int argc, char** argv) { - const char* symmetric_key_file = 0; - const char* key_name = 0; - Options_list options; - options.push_back(Option_def("-k", &key_name)); - options.push_back(Option_def("--key-name", &key_name)); - - int argi = parse_options(options, argc, argv); - if (argc - argi == 0) { - } else if (argc - argi == 1) { - symmetric_key_file = argv[argi]; - } else { - std::clog << "Usage: git-crypt unlock [-k KEYNAME] [KEYFILE]" << std::endl; - return 2; - } - // 0. Make sure working directory is clean (ignoring untracked files) // We do this because we run 'git checkout -f HEAD' later and we don't // want the user to lose any changes. 'git checkout -f HEAD' doesn't touch @@ -669,35 +690,37 @@ int unlock (int argc, char** argv) // mucked with the git config.) std::string path_to_top(get_path_to_top()); - // 3. Install the key - Key_file key_file; - if (symmetric_key_file) { - // Read from the symmetric key file + // 3. Load the key(s) + std::vector key_files; + if (argc > 0) { + // Read from the symmetric key file(s) // TODO: command line flag to accept legacy key format? - if (key_name) { - std::clog << "Error: key name should not be specified when unlocking with symmetric key." << std::endl; - return 1; - } + for (int argi = 0; argi < argc; ++argi) { + const char* symmetric_key_file = argv[argi]; + Key_file key_file; - try { - if (std::strcmp(symmetric_key_file, "-") == 0) { - key_file.load(std::cin); - } else { - if (!key_file.load_from_file(symmetric_key_file)) { - std::clog << "Error: " << symmetric_key_file << ": unable to read key file" << std::endl; - return 1; + try { + if (std::strcmp(symmetric_key_file, "-") == 0) { + key_file.load(std::cin); + } else { + if (!key_file.load_from_file(symmetric_key_file)) { + std::clog << "Error: " << symmetric_key_file << ": unable to read key file" << std::endl; + return 1; + } } + } catch (Key_file::Incompatible) { + std::clog << "Error: " << symmetric_key_file << " is in an incompatible format" << std::endl; + std::clog << "Please upgrade to a newer version of git-crypt." << std::endl; + return 1; + } catch (Key_file::Malformed) { + std::clog << "Error: " << symmetric_key_file << ": not a valid git-crypt key file" << std::endl; + std::clog << "If this key was created prior to git-crypt 0.4, you need to migrate it" << std::endl; + std::clog << "by running 'git-crypt migrate-key /path/to/key/file'." << std::endl; + return 1; } - } catch (Key_file::Incompatible) { - std::clog << "Error: " << symmetric_key_file << " is in an incompatible format" << std::endl; - std::clog << "Please upgrade to a newer version of git-crypt." << std::endl; - return 1; - } catch (Key_file::Malformed) { - std::clog << "Error: " << symmetric_key_file << ": not a valid git-crypt key file" << std::endl; - std::clog << "If this key was created prior to git-crypt 0.4, you need to migrate it" << std::endl; - std::clog << "by running 'git-crypt migrate-key /path/to/key/file'." << std::endl; - return 1; + + key_files.push_back(key_file); } } else { // Decrypt GPG key from root of repo @@ -705,23 +728,29 @@ int unlock (int argc, char** argv) 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, key_name, 0, gpg_secret_keys, repo_keys_path)) { + // TODO: command line option to only unlock specific key instead of all of them + // TODO: avoid decrypting repo keys which are already unlocked in the .git directory + if (!decrypt_repo_keys(key_files, 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(key_file.get_key_name())); - // TODO: croak if internal_key_path already exists??? - mkdir_parent(internal_key_path); - if (!key_file.store_to_file(internal_key_path.c_str())) { - std::clog << "Error: " << internal_key_path << ": unable to write key file" << std::endl; - return 1; - } - // 4. Configure git for git-crypt - configure_git_filters(key_file.get_key_name()); + + // 4. Install the key(s) and configure the git filters + 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())); + // TODO: croak if internal_key_path already exists??? + mkdir_parent(internal_key_path); + if (!key_file->store_to_file(internal_key_path.c_str())) { + std::clog << "Error: " << internal_key_path << ": unable to write key file" << std::endl; + return 1; + } + + configure_git_filters(key_file->get_key_name()); + } // 5. Do a force checkout so any files that were previously checked out encrypted // will now be checked out decrypted. From 3fe85bc928850feed7d38053109a8d9c633e1582 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 5 Jul 2014 11:46:51 -0700 Subject: [PATCH 26/47] Rename add-collab, etc. to add-gpg-key, etc. Since GPG support might be used by a single user and not necessarily among collaborators. --- commands.cpp | 10 +++++----- commands.hpp | 6 +++--- git-crypt.cpp | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/commands.cpp b/commands.cpp index 87d222d..08d0c9c 100644 --- a/commands.cpp +++ b/commands.cpp @@ -780,7 +780,7 @@ int unlock (int argc, char** argv) return 0; } -int add_collab (int argc, char** argv) +int add_gpg_key (int argc, char** argv) { const char* key_name = 0; Options_list options; @@ -863,13 +863,13 @@ int add_collab (int argc, char** argv) return 0; } -int rm_collab (int argc, char** argv) // TODO +int rm_gpg_key (int argc, char** argv) // TODO { - std::clog << "Error: rm-collab is not yet implemented." << std::endl; + std::clog << "Error: rm-gpg-key is not yet implemented." << std::endl; return 1; } -int ls_collabs (int argc, char** argv) // TODO +int ls_gpg_keys (int argc, char** argv) // TODO { // Sketch: // Scan the sub-directories in .git-crypt/keys, outputting something like this: @@ -885,7 +885,7 @@ int ls_collabs (int argc, char** argv) // TODO // To resolve a long hex ID, use a command like this: // gpg --options /dev/null --fixed-list-mode --batch --with-colons --list-keys 0x143DE9B3F7316900 - std::clog << "Error: ls-collabs is not yet implemented." << std::endl; + std::clog << "Error: ls-gpg-keys is not yet implemented." << std::endl; return 1; } diff --git a/commands.hpp b/commands.hpp index dd2448e..8bba666 100644 --- a/commands.hpp +++ b/commands.hpp @@ -46,9 +46,9 @@ int diff (int argc, char** argv); // Public commands: int init (int argc, char** argv); int unlock (int argc, char** argv); -int add_collab (int argc, char** argv); -int rm_collab (int argc, char** argv); -int ls_collabs (int argc, char** argv); +int add_gpg_key (int argc, char** argv); +int rm_gpg_key (int argc, char** argv); +int ls_gpg_keys (int argc, char** argv); int export_key (int argc, char** argv); int keygen (int argc, char** argv); int migrate_key (int argc, char** argv); diff --git a/git-crypt.cpp b/git-crypt.cpp index d270675..36c27c9 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -139,14 +139,14 @@ try { if (std::strcmp(command, "unlock") == 0) { return unlock(argc, argv); } - if (std::strcmp(command, "add-collab") == 0) { - return add_collab(argc, argv); + if (std::strcmp(command, "add-gpg-key") == 0) { + return add_gpg_key(argc, argv); } - if (std::strcmp(command, "rm-collab") == 0) { - return rm_collab(argc, argv); + if (std::strcmp(command, "rm-gpg-key") == 0) { + return rm_gpg_key(argc, argv); } - if (std::strcmp(command, "ls-collabs") == 0) { - return ls_collabs(argc, argv); + if (std::strcmp(command, "ls-gpg-keys") == 0) { + return ls_gpg_keys(argc, argv); } if (std::strcmp(command, "export-key") == 0) { return export_key(argc, argv); From 3d0e7570edd00549c39a740f75d92a11910ffcaf Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 5 Jul 2014 11:46:58 -0700 Subject: [PATCH 27/47] Update usage message --- git-crypt.cpp | 52 ++++++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/git-crypt.cpp b/git-crypt.cpp index 36c27c9..decc889 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -46,32 +46,38 @@ const char* argv0; static void print_usage (std::ostream& out) { out << "Usage: " << argv0 << " COMMAND [ARGS ...]" << std::endl; - out << "" << std::endl; - out << "Standard commands:" << std::endl; - out << " init - generate a key, prepare the current repo to use git-crypt" << std::endl; - out << " unlock KEYFILE - decrypt the current repo using the given symmetric key" << std::endl; - out << " export-key FILE - export the repo's symmetric key to the given file" << std::endl; - //out << " refresh - ensure all files in the repo are properly decrypted" << std::endl; - out << " help - display this help message" << std::endl; - out << " help COMMAND - display help for the given git-crypt command" << std::endl; - out << "" << std::endl; - /* + out << std::endl; + // |--------------------------------------------------------------------------------| 80 characters + out << "Common commands:" << std::endl; + out << " init generate a key and prepare repo to use git-crypt" << std::endl; + out << " status display which files are encrypted" << std::endl; + //out << " refresh ensure all files in the repo are properly decrypted" << std::endl; + out << std::endl; out << "GPG commands:" << std::endl; - out << " unlock - decrypt the current repo using the in-repo GPG-encrypted key" << std::endl; - out << " add-collab GPGID - add the user with the given GPG key ID as a collaborator" << std::endl; - out << " rm-collab GPGID - revoke collaborator status from the given GPG key ID" << std::endl; - out << " ls-collabs - list the GPG key IDs of collaborators" << std::endl; - out << "" << std::endl; - */ + out << " add-gpg-key KEYID add the user with the given GPG key ID as a collaborator" << std::endl; + //out << " rm-gpg-key KEYID revoke collaborator status from the given GPG key ID" << std::endl; + //out << " ls-gpg-keys list the GPG key IDs of collaborators" << std::endl; + out << " unlock decrypt this repo using the in-repo GPG-encrypted key" << std::endl; + out << std::endl; + out << "Symmetric key commands:" << std::endl; + out << " export-key FILE export this repo's symmetric key to the given file" << std::endl; + out << " unlock KEYFILE decrypt this repo using the given symmetric key" << std::endl; + out << std::endl; out << "Legacy commands:" << std::endl; - out << " init KEYFILE - alias for 'unlock KEYFILE'" << std::endl; - out << " keygen KEYFILE - generate a git-crypt key in the given file" << std::endl; - out << " migrate-key FILE - migrate the given legacy key file to the latest format" << std::endl; - out << "" << std::endl; + out << " init KEYFILE alias for 'unlock KEYFILE'" << std::endl; + out << " keygen KEYFILE generate a git-crypt key in the given file" << std::endl; + out << " migrate-key FILE migrate the given legacy key file to the latest format" << std::endl; + /* + out << std::endl; out << "Plumbing commands (not to be used directly):" << std::endl; - out << " clean [LEGACY-KEYFILE]" << std::endl; - out << " smudge [LEGACY-KEYFILE]" << std::endl; - out << " diff [LEGACY-KEYFILE] FILE" << std::endl; + out << " clean [LEGACY-KEYFILE]" << std::endl; + out << " smudge [LEGACY-KEYFILE]" << std::endl; + out << " diff [LEGACY-KEYFILE] FILE" << std::endl; + */ + /* + out << std::endl; + out << "See 'git-crypt help COMMAND' for more information on a specific command." << std::endl; + */ } From d417f97f8ea7c03a06dc7b8c89d00f70a684f529 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 5 Jul 2014 14:22:55 -0700 Subject: [PATCH 28/47] Make argv arrays const --- commands.cpp | 28 ++++++++++++++-------------- commands.hpp | 26 +++++++++++++------------- git-crypt.cpp | 2 +- parse_options.cpp | 4 ++-- parse_options.hpp | 2 +- 5 files changed, 31 insertions(+), 31 deletions(-) diff --git a/commands.cpp b/commands.cpp index 08d0c9c..7d93f65 100644 --- a/commands.cpp +++ b/commands.cpp @@ -389,7 +389,7 @@ static void encrypt_repo_key (const char* key_name, const Key_file::Entry& key, } } -static int parse_plumbing_options (const char** key_name, const char** key_file, int argc, char** argv) +static int parse_plumbing_options (const char** key_name, const char** key_file, int argc, const char** argv) { Options_list options; options.push_back(Option_def("-k", key_name)); @@ -402,7 +402,7 @@ static int parse_plumbing_options (const char** key_name, const char** key_file, // Encrypt contents of stdin and write to stdout -int clean (int argc, char** argv) +int clean (int argc, const char** argv) { const char* key_name = 0; const char* key_path = 0; @@ -522,7 +522,7 @@ int clean (int argc, char** argv) } // Decrypt contents of stdin and write to stdout -int smudge (int argc, char** argv) +int smudge (int argc, const char** argv) { const char* key_name = 0; const char* key_path = 0; @@ -559,7 +559,7 @@ int smudge (int argc, char** argv) return 0; } -int diff (int argc, char** argv) +int diff (int argc, const char** argv) { const char* key_name = 0; const char* key_path = 0; @@ -611,7 +611,7 @@ int diff (int argc, char** argv) return 0; } -int init (int argc, char** argv) +int init (int argc, const char** argv) { const char* key_name = 0; Options_list options; @@ -661,7 +661,7 @@ int init (int argc, char** argv) return 0; } -int unlock (int argc, char** argv) +int unlock (int argc, const char** argv) { // 0. Make sure working directory is clean (ignoring untracked files) // We do this because we run 'git checkout -f HEAD' later and we don't @@ -780,7 +780,7 @@ int unlock (int argc, char** argv) return 0; } -int add_gpg_key (int argc, char** argv) +int add_gpg_key (int argc, const char** argv) { const char* key_name = 0; Options_list options; @@ -863,13 +863,13 @@ int add_gpg_key (int argc, char** argv) return 0; } -int rm_gpg_key (int argc, char** argv) // TODO +int rm_gpg_key (int argc, const char** argv) // TODO { std::clog << "Error: rm-gpg-key is not yet implemented." << std::endl; return 1; } -int ls_gpg_keys (int argc, char** argv) // TODO +int ls_gpg_keys (int argc, const char** argv) // TODO { // Sketch: // Scan the sub-directories in .git-crypt/keys, outputting something like this: @@ -889,7 +889,7 @@ int ls_gpg_keys (int argc, char** argv) // TODO return 1; } -int export_key (int argc, char** argv) +int export_key (int argc, const char** argv) { // TODO: provide options to export only certain key versions const char* key_name = 0; @@ -921,7 +921,7 @@ int export_key (int argc, char** argv) return 0; } -int keygen (int argc, char** argv) +int keygen (int argc, const char** argv) { if (argc != 1) { std::clog << "Usage: git-crypt keygen KEYFILE" << std::endl; @@ -950,7 +950,7 @@ int keygen (int argc, char** argv) return 0; } -int migrate_key (int argc, char** argv) +int migrate_key (int argc, const char** argv) { if (argc != 1) { std::clog << "Usage: git-crypt migrate-key KEYFILE" << std::endl; @@ -1000,13 +1000,13 @@ int migrate_key (int argc, char** argv) return 0; } -int refresh (int argc, char** argv) // TODO: do a force checkout, much like in unlock +int refresh (int argc, const char** argv) // TODO: do a force checkout, much like in unlock { std::clog << "Error: refresh is not yet implemented." << std::endl; return 1; } -int status (int argc, char** argv) +int status (int argc, const char** argv) { // Usage: // git-crypt status -r [-z] Show repo status diff --git a/commands.hpp b/commands.hpp index 8bba666..2575ca6 100644 --- a/commands.hpp +++ b/commands.hpp @@ -40,20 +40,20 @@ struct Error { }; // Plumbing commands: -int clean (int argc, char** argv); -int smudge (int argc, char** argv); -int diff (int argc, char** argv); +int clean (int argc, const char** argv); +int smudge (int argc, const char** argv); +int diff (int argc, const char** argv); // Public commands: -int init (int argc, char** argv); -int unlock (int argc, char** argv); -int add_gpg_key (int argc, char** argv); -int rm_gpg_key (int argc, char** argv); -int ls_gpg_keys (int argc, char** argv); -int export_key (int argc, char** argv); -int keygen (int argc, char** argv); -int migrate_key (int argc, char** argv); -int refresh (int argc, char** argv); -int status (int argc, char** argv); +int init (int argc, const char** argv); +int unlock (int argc, const char** argv); +int add_gpg_key (int argc, const char** argv); +int rm_gpg_key (int argc, const char** argv); +int ls_gpg_keys (int argc, const char** argv); +int export_key (int argc, const char** argv); +int keygen (int argc, const char** argv); +int migrate_key (int argc, const char** argv); +int refresh (int argc, const char** argv); +int status (int argc, const char** argv); #endif diff --git a/git-crypt.cpp b/git-crypt.cpp index decc889..58b9923 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -81,7 +81,7 @@ static void print_usage (std::ostream& out) } -int main (int argc, char** argv) +int main (int argc, const char** argv) try { argv0 = argv[0]; diff --git a/parse_options.cpp b/parse_options.cpp index dc93133..51b51f7 100644 --- a/parse_options.cpp +++ b/parse_options.cpp @@ -42,7 +42,7 @@ static const Option_def* find_option (const Options_list& options, const std::st return 0; } -int parse_options (const Options_list& options, int argc, char** argv) +int parse_options (const Options_list& options, int argc, const char** argv) { int argi = 0; @@ -53,7 +53,7 @@ int parse_options (const Options_list& options, int argc, char** argv) } else if (std::strncmp(argv[argi], "--", 2) == 0) { std::string option_name; const char* option_value = 0; - if (char* eq = std::strchr(argv[argi], '=')) { + if (const char* eq = std::strchr(argv[argi], '=')) { option_name.assign(argv[argi], eq); option_value = eq + 1; } else { diff --git a/parse_options.hpp b/parse_options.hpp index d02ddaa..c0580f0 100644 --- a/parse_options.hpp +++ b/parse_options.hpp @@ -48,7 +48,7 @@ struct Option_def { typedef std::vector Options_list; -int parse_options (const Options_list& options, int argc, char** argv); +int parse_options (const Options_list& options, int argc, const char** argv); struct Option_error { std::string option_name; From f6e3b63a93d57fd39565f8ce777085b22cc74a8c Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Mon, 7 Jul 2014 21:49:12 -0700 Subject: [PATCH 29/47] Makefile: avoid use of non-standard $^ --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0035245..142e92d 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ OBJFILES = git-crypt.o commands.o crypto.o gpg.o key.o util.o parse_options.o all: git-crypt git-crypt: $(OBJFILES) - $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) + $(CXX) $(CXXFLAGS) -o $@ $(OBJFILES) $(LDFLAGS) util.o: util.cpp util-unix.cpp util-win32.cpp From 66a2266968fdad95a3e4183b084fff0601eaf533 Mon Sep 17 00:00:00 2001 From: Jon Sailor Date: Sun, 6 Jul 2014 02:33:35 -0700 Subject: [PATCH 30/47] Pull out openssl code into separate `crypto-openssl.cpp` file This will allow the use of different crypto libraries in the future. Modified-by: Andrew Ayer * Don't include openssl/err.h from git-crypt.cpp * Fix whitespace and other style to conform to project conventions * Remove unnecessary operators from Aes_ctr_encryptor * Rename crypto_init to init_crypto, for consistency with init_std_streams() --- Makefile | 14 +++++- crypto-openssl.cpp | 108 +++++++++++++++++++++++++++++++++++++++++++++ crypto.cpp | 50 +-------------------- crypto.hpp | 38 +++++++++++++--- git-crypt.cpp | 3 +- 5 files changed, 154 insertions(+), 59 deletions(-) create mode 100644 crypto-openssl.cpp diff --git a/Makefile b/Makefile index 142e92d..bef297d 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,19 @@ CXX := c++ CXXFLAGS := -Wall -pedantic -Wno-long-long -O2 -LDFLAGS := -lcrypto +LDFLAGS := PREFIX := /usr/local -OBJFILES = git-crypt.o commands.o crypto.o gpg.o key.o util.o parse_options.o +OBJFILES = \ + git-crypt.o \ + commands.o \ + crypto.o \ + gpg.o \ + key.o \ + util.o \ + parse_options.o + +OBJFILES += crypto-openssl.o +LDFLAGS += -lcrypto all: git-crypt diff --git a/crypto-openssl.cpp b/crypto-openssl.cpp new file mode 100644 index 0000000..6ae8293 --- /dev/null +++ b/crypto-openssl.cpp @@ -0,0 +1,108 @@ +/* + * Copyright 2012, 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 "crypto.hpp" +#include "key.hpp" +#include +#include +#include +#include +#include +#include +#include + +void init_crypto () +{ + ERR_load_crypto_strings(); +} + +struct Aes_impl { + AES_KEY key; +}; + +Aes_ecb_encryptor::Aes_ecb_encryptor (const unsigned char* raw_key) +{ + impl = new Aes_impl; + if (AES_set_encrypt_key(raw_key, KEY_LEN * 8, &(impl->key)) != 0) { + throw Crypto_error("Aes_ctr_encryptor::Aes_ctr_encryptor", "AES_set_encrypt_key failed"); + } +} + +Aes_ecb_encryptor::~Aes_ecb_encryptor () +{ + delete impl; +} + +void Aes_ecb_encryptor::encrypt(const unsigned char* plain, unsigned char* cipher) +{ + AES_encrypt(plain, cipher, &(impl->key)); +} + +struct Hmac_impl { + HMAC_CTX ctx; +}; + +Hmac_sha1_state::Hmac_sha1_state (const unsigned char* key, size_t key_len) +{ + impl = new Hmac_impl; + HMAC_Init(&(impl->ctx), key, key_len, EVP_sha1()); +} + +Hmac_sha1_state::~Hmac_sha1_state () +{ + HMAC_cleanup(&(impl->ctx)); + delete impl; +} + +void Hmac_sha1_state::add (const unsigned char* buffer, size_t buffer_len) +{ + HMAC_Update(&(impl->ctx), buffer, buffer_len); +} + +void Hmac_sha1_state::get (unsigned char* digest) +{ + unsigned int len; + HMAC_Final(&(impl->ctx), digest, &len); +} + + +void random_bytes (unsigned char* buffer, size_t len) +{ + if (RAND_bytes(buffer, len) != 1) { + std::ostringstream message; + while (unsigned long code = ERR_get_error()) { + char error_string[120]; + ERR_error_string_n(code, error_string, sizeof(error_string)); + message << "OpenSSL Error: " << error_string << "; "; + } + throw Crypto_error("random_bytes", message.str()); + } +} + diff --git a/crypto.cpp b/crypto.cpp index c11d5e2..db081ae 100644 --- a/crypto.cpp +++ b/crypto.cpp @@ -30,22 +30,11 @@ #include "crypto.hpp" #include "util.hpp" -#include -#include -#include -#include -#include -#include -#include #include -#include Aes_ctr_encryptor::Aes_ctr_encryptor (const unsigned char* raw_key, const unsigned char* arg_nonce) +: ecb(raw_key) { - if (AES_set_encrypt_key(raw_key, KEY_LEN * 8, &key) != 0) { - throw Crypto_error("Aes_ctr_encryptor::Aes_ctr_encryptor", "AES_set_encrypt_key failed"); - } - std::memcpy(nonce, arg_nonce, NONCE_LEN); byte_counter = 0; std::memset(otp, '\0', sizeof(otp)); @@ -64,7 +53,7 @@ void Aes_ctr_encryptor::process (const unsigned char* in, unsigned char* out, si store_be32(ctr + NONCE_LEN, byte_counter / BLOCK_LEN); // Generate a new OTP - AES_encrypt(ctr, otp, &key); + ecb.encrypt(ctr, otp); } // encrypt one byte @@ -76,28 +65,6 @@ void Aes_ctr_encryptor::process (const unsigned char* in, unsigned char* out, si } } -Hmac_sha1_state::Hmac_sha1_state (const unsigned char* key, size_t key_len) -{ - HMAC_Init(&ctx, key, key_len, EVP_sha1()); -} - -Hmac_sha1_state::~Hmac_sha1_state () -{ - HMAC_cleanup(&ctx); -} - -void Hmac_sha1_state::add (const unsigned char* buffer, size_t buffer_len) -{ - HMAC_Update(&ctx, buffer, buffer_len); -} - -void Hmac_sha1_state::get (unsigned char* digest) -{ - unsigned int len; - HMAC_Final(&ctx, digest, &len); -} - - // Encrypt/decrypt an entire input stream, writing to the given output stream void Aes_ctr_encryptor::process_stream (std::istream& in, std::ostream& out, const unsigned char* key, const unsigned char* nonce) { @@ -111,16 +78,3 @@ void Aes_ctr_encryptor::process_stream (std::istream& in, std::ostream& out, con } } -void random_bytes (unsigned char* buffer, size_t len) -{ - if (RAND_bytes(buffer, len) != 1) { - std::ostringstream message; - while (unsigned long code = ERR_get_error()) { - char error_string[120]; - ERR_error_string_n(code, error_string, sizeof(error_string)); - message << "OpenSSL Error: " << error_string << "; "; - } - throw Crypto_error("random_bytes", message.str()); - } -} - diff --git a/crypto.hpp b/crypto.hpp index 63772a1..ae6a14c 100644 --- a/crypto.hpp +++ b/crypto.hpp @@ -32,13 +32,13 @@ #define GIT_CRYPT_CRYPTO_HPP #include "key.hpp" -#include -#include #include #include #include #include +void init_crypto (); + struct Crypto_error { std::string where; std::string message; @@ -46,6 +46,28 @@ struct Crypto_error { Crypto_error (const std::string& w, const std::string& m) : where(w), message(m) { } }; +struct Aes_impl; + +class Aes_ecb_encryptor { +public: + enum { + KEY_LEN = AES_KEY_LEN, + BLOCK_LEN = 16 + }; + +private: + Aes_impl* impl; + + // disallow copy/assignment: + Aes_ecb_encryptor (const Aes_ecb_encryptor&); + Aes_ecb_encryptor& operator= (const Aes_ecb_encryptor&); + +public: + Aes_ecb_encryptor (const unsigned char* key); + ~Aes_ecb_encryptor (); + void encrypt (const unsigned char* plain, unsigned char* cipher); +}; + class Aes_ctr_encryptor { public: enum { @@ -56,10 +78,10 @@ public: }; private: - AES_KEY key; - char nonce[NONCE_LEN];// First 96 bits of counter - uint32_t byte_counter; // How many bytes processed so far? - unsigned char otp[BLOCK_LEN]; // The current OTP that's in use + Aes_ecb_encryptor ecb; + char nonce[NONCE_LEN];// First 96 bits of counter + uint32_t byte_counter; // How many bytes processed so far? + unsigned char otp[BLOCK_LEN]; // The current OTP that's in use public: Aes_ctr_encryptor (const unsigned char* key, const unsigned char* nonce); @@ -72,6 +94,8 @@ public: typedef Aes_ctr_encryptor Aes_ctr_decryptor; +struct Hmac_impl; + class Hmac_sha1_state { public: enum { @@ -80,7 +104,7 @@ public: }; private: - HMAC_CTX ctx; + Hmac_impl* impl; // disallow copy/assignment: Hmac_sha1_state (const Hmac_sha1_state&) { } diff --git a/git-crypt.cpp b/git-crypt.cpp index 58b9923..e2cc9fc 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -39,7 +39,6 @@ #include #include #include -#include const char* argv0; @@ -90,7 +89,7 @@ try { */ init_std_streams(); - ERR_load_crypto_strings(); + init_crypto(); /* * Parse command line arguments From 0210fd7541c172ec213ceeae169df334b8d19d9c Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Mon, 7 Jul 2014 22:28:07 -0700 Subject: [PATCH 31/47] Use auto_ptr instead of explicit memory management --- crypto-openssl.cpp | 11 +++++++---- crypto.hpp | 13 +++---------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/crypto-openssl.cpp b/crypto-openssl.cpp index 6ae8293..48d7af5 100644 --- a/crypto-openssl.cpp +++ b/crypto-openssl.cpp @@ -48,8 +48,8 @@ struct Aes_impl { }; Aes_ecb_encryptor::Aes_ecb_encryptor (const unsigned char* raw_key) +: impl(new Aes_impl) { - impl = new Aes_impl; if (AES_set_encrypt_key(raw_key, KEY_LEN * 8, &(impl->key)) != 0) { throw Crypto_error("Aes_ctr_encryptor::Aes_ctr_encryptor", "AES_set_encrypt_key failed"); } @@ -57,7 +57,8 @@ Aes_ecb_encryptor::Aes_ecb_encryptor (const unsigned char* raw_key) Aes_ecb_encryptor::~Aes_ecb_encryptor () { - delete impl; + // Note: Explicit destructor necessary because class contains an auto_ptr + // which contains an incomplete type when the auto_ptr is declared. } void Aes_ecb_encryptor::encrypt(const unsigned char* plain, unsigned char* cipher) @@ -70,15 +71,17 @@ struct Hmac_impl { }; Hmac_sha1_state::Hmac_sha1_state (const unsigned char* key, size_t key_len) +: impl(new Hmac_impl) { - impl = new Hmac_impl; HMAC_Init(&(impl->ctx), key, key_len, EVP_sha1()); } Hmac_sha1_state::~Hmac_sha1_state () { + // Note: Explicit destructor necessary because class contains an auto_ptr + // which contains an incomplete type when the auto_ptr is declared. + HMAC_cleanup(&(impl->ctx)); - delete impl; } void Hmac_sha1_state::add (const unsigned char* buffer, size_t buffer_len) diff --git a/crypto.hpp b/crypto.hpp index ae6a14c..4eedc07 100644 --- a/crypto.hpp +++ b/crypto.hpp @@ -36,6 +36,7 @@ #include #include #include +#include void init_crypto (); @@ -56,11 +57,7 @@ public: }; private: - Aes_impl* impl; - - // disallow copy/assignment: - Aes_ecb_encryptor (const Aes_ecb_encryptor&); - Aes_ecb_encryptor& operator= (const Aes_ecb_encryptor&); + std::auto_ptr impl; public: Aes_ecb_encryptor (const unsigned char* key); @@ -104,11 +101,7 @@ public: }; private: - Hmac_impl* impl; - - // disallow copy/assignment: - Hmac_sha1_state (const Hmac_sha1_state&) { } - Hmac_sha1_state& operator= (const Hmac_sha1_state&) { return *this; } + std::auto_ptr impl; public: Hmac_sha1_state (const unsigned char* key, size_t key_len); From 22bae167b0927798df16506b5ab6251d287d46fc Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Mon, 7 Jul 2014 22:29:13 -0700 Subject: [PATCH 32/47] Make Aes_impl and Hmac_impl private member classes --- crypto-openssl.cpp | 4 ++-- crypto.hpp | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crypto-openssl.cpp b/crypto-openssl.cpp index 48d7af5..cb168fc 100644 --- a/crypto-openssl.cpp +++ b/crypto-openssl.cpp @@ -43,7 +43,7 @@ void init_crypto () ERR_load_crypto_strings(); } -struct Aes_impl { +struct Aes_ecb_encryptor::Aes_impl { AES_KEY key; }; @@ -66,7 +66,7 @@ void Aes_ecb_encryptor::encrypt(const unsigned char* plain, unsigned char* ciphe AES_encrypt(plain, cipher, &(impl->key)); } -struct Hmac_impl { +struct Hmac_sha1_state::Hmac_impl { HMAC_CTX ctx; }; diff --git a/crypto.hpp b/crypto.hpp index 4eedc07..adc9643 100644 --- a/crypto.hpp +++ b/crypto.hpp @@ -47,8 +47,6 @@ struct Crypto_error { Crypto_error (const std::string& w, const std::string& m) : where(w), message(m) { } }; -struct Aes_impl; - class Aes_ecb_encryptor { public: enum { @@ -57,6 +55,8 @@ public: }; private: + struct Aes_impl; + std::auto_ptr impl; public: @@ -91,8 +91,6 @@ public: typedef Aes_ctr_encryptor Aes_ctr_decryptor; -struct Hmac_impl; - class Hmac_sha1_state { public: enum { @@ -101,6 +99,8 @@ public: }; private: + struct Hmac_impl; + std::auto_ptr impl; public: From 8de40f40b3a1512bd8c7f3786cda9234b04b2776 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Mon, 7 Jul 2014 22:41:13 -0700 Subject: [PATCH 33/47] Wipe AES key from memory after using it --- crypto-openssl.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crypto-openssl.cpp b/crypto-openssl.cpp index cb168fc..e833ada 100644 --- a/crypto-openssl.cpp +++ b/crypto-openssl.cpp @@ -37,6 +37,7 @@ #include #include #include +#include void init_crypto () { @@ -59,6 +60,8 @@ Aes_ecb_encryptor::~Aes_ecb_encryptor () { // Note: Explicit destructor necessary because class contains an auto_ptr // which contains an incomplete type when the auto_ptr is declared. + + std::memset(&impl->key, '\0', sizeof(impl->key)); } void Aes_ecb_encryptor::encrypt(const unsigned char* plain, unsigned char* cipher) From 23ff272f7d022eec3f242b3b44cebd7cf00f90a5 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Mon, 7 Jul 2014 22:52:12 -0700 Subject: [PATCH 34/47] Simplify CTR code --- crypto.cpp | 27 ++++++++++++++------------- crypto.hpp | 7 ++++--- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/crypto.cpp b/crypto.cpp index db081ae..f2d9d28 100644 --- a/crypto.cpp +++ b/crypto.cpp @@ -32,32 +32,33 @@ #include "util.hpp" #include -Aes_ctr_encryptor::Aes_ctr_encryptor (const unsigned char* raw_key, const unsigned char* arg_nonce) +Aes_ctr_encryptor::Aes_ctr_encryptor (const unsigned char* raw_key, const unsigned char* nonce) : ecb(raw_key) { - std::memcpy(nonce, arg_nonce, NONCE_LEN); + // Set first 12 bytes of the CTR value to the nonce. + // This stays the same for the entirety of this object's lifetime. + std::memcpy(ctr_value, nonce, NONCE_LEN); byte_counter = 0; - std::memset(otp, '\0', sizeof(otp)); +} + +Aes_ctr_encryptor::~Aes_ctr_encryptor () +{ + std::memset(pad, '\0', BLOCK_LEN); } void Aes_ctr_encryptor::process (const unsigned char* in, unsigned char* out, size_t len) { for (size_t i = 0; i < len; ++i) { if (byte_counter % BLOCK_LEN == 0) { - unsigned char ctr[BLOCK_LEN]; + // Set last 4 bytes of CTR to the (big-endian) block number (sequentially increasing with each block) + store_be32(ctr_value + NONCE_LEN, byte_counter / BLOCK_LEN); - // First 12 bytes of CTR: nonce - std::memcpy(ctr, nonce, NONCE_LEN); - - // Last 4 bytes of CTR: block number (sequentially increasing with each block) (big endian) - store_be32(ctr + NONCE_LEN, byte_counter / BLOCK_LEN); - - // Generate a new OTP - ecb.encrypt(ctr, otp); + // Generate a new pad + ecb.encrypt(ctr_value, pad); } // encrypt one byte - out[i] = in[i] ^ otp[byte_counter++ % BLOCK_LEN]; + out[i] = in[i] ^ pad[byte_counter++ % BLOCK_LEN]; if (byte_counter == 0) { throw Crypto_error("Aes_ctr_encryptor::process", "Too much data to encrypt securely"); diff --git a/crypto.hpp b/crypto.hpp index adc9643..db03241 100644 --- a/crypto.hpp +++ b/crypto.hpp @@ -76,12 +76,13 @@ public: private: Aes_ecb_encryptor ecb; - char nonce[NONCE_LEN];// First 96 bits of counter - uint32_t byte_counter; // How many bytes processed so far? - unsigned char otp[BLOCK_LEN]; // The current OTP that's in use + unsigned char ctr_value[BLOCK_LEN]; // Current CTR value (used as input to AES to derive pad) + unsigned char pad[BLOCK_LEN]; // Current encryption pad (output of AES) + uint32_t byte_counter; // How many bytes processed so far? public: Aes_ctr_encryptor (const unsigned char* key, const unsigned char* nonce); + ~Aes_ctr_encryptor (); void process (const unsigned char* in, unsigned char* out, size_t len); From 477983f4bc14dae3131bd80282a94e1c67695c0d Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Wed, 23 Jul 2014 19:32:30 -0700 Subject: [PATCH 35/47] Ensure memsets of sensitive memory aren't optimized away --- crypto-openssl.cpp | 3 ++- crypto.cpp | 2 +- key.cpp | 4 ++-- util.cpp | 11 +++++++++++ util.hpp | 1 + 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/crypto-openssl.cpp b/crypto-openssl.cpp index e833ada..6483e86 100644 --- a/crypto-openssl.cpp +++ b/crypto-openssl.cpp @@ -30,6 +30,7 @@ #include "crypto.hpp" #include "key.hpp" +#include "util.hpp" #include #include #include @@ -61,7 +62,7 @@ Aes_ecb_encryptor::~Aes_ecb_encryptor () // Note: Explicit destructor necessary because class contains an auto_ptr // which contains an incomplete type when the auto_ptr is declared. - std::memset(&impl->key, '\0', sizeof(impl->key)); + explicit_memset(&impl->key, '\0', sizeof(impl->key)); } void Aes_ecb_encryptor::encrypt(const unsigned char* plain, unsigned char* cipher) diff --git a/crypto.cpp b/crypto.cpp index f2d9d28..3ae3ecb 100644 --- a/crypto.cpp +++ b/crypto.cpp @@ -43,7 +43,7 @@ Aes_ctr_encryptor::Aes_ctr_encryptor (const unsigned char* raw_key, const unsign Aes_ctr_encryptor::~Aes_ctr_encryptor () { - std::memset(pad, '\0', BLOCK_LEN); + explicit_memset(pad, '\0', BLOCK_LEN); } void Aes_ctr_encryptor::process (const unsigned char* in, unsigned char* out, size_t len) diff --git a/key.cpp b/key.cpp index 552ae38..0ae24b8 100644 --- a/key.cpp +++ b/key.cpp @@ -45,8 +45,8 @@ Key_file::Entry::Entry () { version = 0; - std::memset(aes_key, 0, AES_KEY_LEN); - std::memset(hmac_key, 0, HMAC_KEY_LEN); + explicit_memset(aes_key, 0, AES_KEY_LEN); + explicit_memset(hmac_key, 0, HMAC_KEY_LEN); } void Key_file::Entry::load (std::istream& in) diff --git a/util.cpp b/util.cpp index 84e8253..189e52a 100644 --- a/util.cpp +++ b/util.cpp @@ -81,6 +81,17 @@ void write_be32 (std::ostream& out, uint32_t i) out.write(reinterpret_cast(buffer), 4); } +void* explicit_memset (void* s, int c, std::size_t n) +{ + volatile unsigned char* p = reinterpret_cast(s); + + while (n--) { + *p++ = c; + } + + return s; +} + static void init_std_streams_platform (); // platform-specific initialization void init_std_streams () diff --git a/util.hpp b/util.hpp index 107cdfc..e79d805 100644 --- a/util.hpp +++ b/util.hpp @@ -70,6 +70,7 @@ uint32_t load_be32 (const unsigned char*); void store_be32 (unsigned char*, uint32_t); bool read_be32 (std::istream& in, uint32_t&); void write_be32 (std::ostream& out, uint32_t); +void* explicit_memset (void* s, int c, size_t n); // memset that won't be optimized away void init_std_streams (); mode_t util_umask (mode_t); int util_rename (const char*, const char*); From 9e791d97ee43d06626a4c79ac8769a0a945d3583 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Wed, 23 Jul 2014 19:55:50 -0700 Subject: [PATCH 36/47] Factor out some common code into a helper function --- commands.cpp | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/commands.cpp b/commands.cpp index 7d93f65..2ed5254 100644 --- a/commands.cpp +++ b/commands.cpp @@ -521,6 +521,21 @@ int clean (int argc, const char** argv) return 0; } +static int decrypt_file_to_stdout (const Key_file& key_file, const unsigned char* header, std::istream& in) +{ + const unsigned char* nonce = header + 10; + uint32_t key_version = 0; // TODO: get the version from the file header + + const Key_file::Entry* key = key_file.get(key_version); + if (!key) { + std::clog << "git-crypt: error: key version " << key_version << " not available - please unlock with the latest version of the key." << std::endl; + return 1; + } + + Aes_ctr_decryptor::process_stream(in, std::cout, key->aes_key, nonce); + return 0; +} + // Decrypt contents of stdin and write to stdout int smudge (int argc, const char** argv) { @@ -546,17 +561,8 @@ int smudge (int argc, const char** argv) std::clog << "git-crypt: error: file not encrypted" << std::endl; return 1; } - const unsigned char* nonce = header + 10; - uint32_t key_version = 0; // TODO: get the version from the file header - const Key_file::Entry* key = key_file.get(key_version); - if (!key) { - std::clog << "git-crypt: error: key version " << key_version << " not available - please unlock with the latest version of the key." << std::endl; - return 1; - } - - Aes_ctr_decryptor::process_stream(std::cin, std::cout, key->aes_key, nonce); - return 0; + return decrypt_file_to_stdout(key_file, header, std::cin); } int diff (int argc, const char** argv) @@ -598,17 +604,7 @@ int diff (int argc, const char** argv) } // Go ahead and decrypt it - const unsigned char* nonce = header + 10; - uint32_t key_version = 0; // TODO: get the version from the file header - - const Key_file::Entry* key = key_file.get(key_version); - if (!key) { - std::clog << "git-crypt: error: key version " << key_version << " not available - please unlock with the latest version of the key." << std::endl; - return 1; - } - - Aes_ctr_decryptor::process_stream(in, std::cout, key->aes_key, nonce); - return 0; + return decrypt_file_to_stdout(key_file, header, in); } int init (int argc, const char** argv) From 01f152b746a9d64c1d0fde930d9a4f9bee770fd0 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Wed, 23 Jul 2014 19:58:13 -0700 Subject: [PATCH 37/47] Check HMAC in smudge and diff commands Git-crypt's position has always been that authentication is best left to Git, since 1) Git provides immutable history based on SHA-1 hashes as well as GPG-signed commits and tags, and 2) git-crypt can't be used safely anyways unless the overall integrity of your repository is assured. But, since git-crypt already has easy access to a (truncated) HMAC of the file when decrypting, there's really no reason why git-crypt shouldn't just verify it and provide an additional layer of protection. --- commands.cpp | 21 ++++++++++++++++++++- util.cpp | 17 +++++++++++++++++ util.hpp | 1 + 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/commands.cpp b/commands.cpp index 2ed5254..0e5f7df 100644 --- a/commands.cpp +++ b/commands.cpp @@ -532,7 +532,26 @@ static int decrypt_file_to_stdout (const Key_file& key_file, const unsigned char return 1; } - Aes_ctr_decryptor::process_stream(in, std::cout, key->aes_key, nonce); + Aes_ctr_decryptor aes(key->aes_key, nonce); + Hmac_sha1_state hmac(key->hmac_key, HMAC_KEY_LEN); + while (in) { + unsigned char buffer[1024]; + in.read(reinterpret_cast(buffer), sizeof(buffer)); + aes.process(buffer, buffer, in.gcount()); + hmac.add(buffer, in.gcount()); + std::cout.write(reinterpret_cast(buffer), in.gcount()); + } + + unsigned char digest[Hmac_sha1_state::LEN]; + hmac.get(digest); + if (!leakless_equals(digest, nonce, Aes_ctr_decryptor::NONCE_LEN)) { + std::clog << "git-crypt: error: encrypted file has been tampered with!" << std::endl; + // Although we've already written the tampered file to stdout, exiting + // with a non-zero status will tell git the file has not been filtered, + // so git will not replace it. + return 1; + } + return 0; } diff --git a/util.cpp b/util.cpp index 189e52a..2da0622 100644 --- a/util.cpp +++ b/util.cpp @@ -92,6 +92,23 @@ void* explicit_memset (void* s, int c, std::size_t n) return s; } +static bool leakless_equals_char (const unsigned char* a, const unsigned char* b, std::size_t len) +{ + volatile int diff = 0; + + while (len > 0) { + diff |= *a++ ^ *b++; + --len; + } + + return diff == 0; +} + +bool leakless_equals (const void* a, const void* b, std::size_t len) +{ + return leakless_equals_char(reinterpret_cast(a), reinterpret_cast(b), len); +} + static void init_std_streams_platform (); // platform-specific initialization void init_std_streams () diff --git a/util.hpp b/util.hpp index e79d805..8281294 100644 --- a/util.hpp +++ b/util.hpp @@ -71,6 +71,7 @@ void store_be32 (unsigned char*, uint32_t); bool read_be32 (std::istream& in, uint32_t&); void write_be32 (std::ostream& out, uint32_t); void* explicit_memset (void* s, int c, size_t n); // memset that won't be optimized away +bool leakless_equals (const void* a, const void* b, size_t len); // compare bytes w/o leaking timing void init_std_streams (); mode_t util_umask (mode_t); int util_rename (const char*, const char*); From 47e810d592c330ed62038410b7a8ebadc628333d Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 2 Aug 2014 20:59:48 -0700 Subject: [PATCH 38/47] Write and use create_protected_file() helper Instead of using umask to ensure sensitive files are created with restrictive permissions, git-crypt now does: create_protected_file(filename); std::ofstream out(filename); // ... create_protected_file can have different Unix and Windows implementations. create_protected_file should be easier to implement on Windows than a umask equivalent, and this pattern keeps the amount of platform-specific code to a minimum and avoids #ifdefs. --- key.cpp | 3 +-- util-unix.cpp | 15 ++++++++++----- util-win32.cpp | 4 +--- util.hpp | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/key.cpp b/key.cpp index 0ae24b8..2da3b8d 100644 --- a/key.cpp +++ b/key.cpp @@ -261,9 +261,8 @@ bool Key_file::load_from_file (const char* key_file_name) bool Key_file::store_to_file (const char* key_file_name) const { - mode_t old_umask = util_umask(0077); // make sure key file is protected + create_protected_file(key_file_name); std::ofstream key_file_out(key_file_name, std::fstream::binary); - util_umask(old_umask); if (!key_file_out) { return false; } diff --git a/util-unix.cpp b/util-unix.cpp index 2385566..2bdf364 100644 --- a/util-unix.cpp +++ b/util-unix.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -72,14 +73,14 @@ void temp_fstream::open (std::ios_base::openmode mode) char* path = &path_buffer[0]; std::strcpy(path, tmpdir); std::strcpy(path + tmpdir_len, "/git-crypt.XXXXXX"); - mode_t old_umask = util_umask(0077); + mode_t old_umask = umask(0077); int fd = mkstemp(path); if (fd == -1) { int mkstemp_errno = errno; - util_umask(old_umask); + umask(old_umask); throw System_error("mkstemp", "", mkstemp_errno); } - util_umask(old_umask); + umask(old_umask); std::fstream::open(path, mode); if (!std::fstream::is_open()) { unlink(path); @@ -288,9 +289,13 @@ static void init_std_streams_platform () { } -mode_t util_umask (mode_t mode) +void create_protected_file (const char* path) { - return umask(mode); + int fd = open(path, O_WRONLY | O_CREAT, 0600); + if (fd == -1) { + throw System_error("open", path, errno); + } + close(fd); } int util_rename (const char* from, const char* to) diff --git a/util-win32.cpp b/util-win32.cpp index 4e6e9c1..4d442db 100644 --- a/util-win32.cpp +++ b/util-win32.cpp @@ -346,10 +346,8 @@ static void init_std_streams_platform () _setmode(_fileno(stdout), _O_BINARY); } -mode_t util_umask (mode_t mode) +void create_protected_file (const char* path) // TODO { - // Not available in Windows and function not always defined in Win32 environments - return 0; } int util_rename (const char* from, const char* to) diff --git a/util.hpp b/util.hpp index 8281294..aa04912 100644 --- a/util.hpp +++ b/util.hpp @@ -73,7 +73,7 @@ void write_be32 (std::ostream& out, uint32_t); void* explicit_memset (void* s, int c, size_t n); // memset that won't be optimized away bool leakless_equals (const void* a, const void* b, size_t len); // compare bytes w/o leaking timing void init_std_streams (); -mode_t util_umask (mode_t); +void create_protected_file (const char* path); // create empty file accessible only by current user int util_rename (const char*, const char*); std::vector get_directory_contents (const char* path); From da25322dbc604414d1b4facc9a81be3706293f81 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 2 Aug 2014 21:23:52 -0700 Subject: [PATCH 39/47] Remove stubs for profile support Multiple key support provides the functionality I was planning to provide with profiles. --- git-crypt.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/git-crypt.cpp b/git-crypt.cpp index e2cc9fc..d0567ab 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -94,18 +94,11 @@ try { /* * Parse command line arguments */ - const char* profile = 0; int arg_index = 1; while (arg_index < argc && argv[arg_index][0] == '-') { if (std::strcmp(argv[arg_index], "--help") == 0) { print_usage(std::clog); return 0; - } else if (std::strncmp(argv[arg_index], "--profile=", 10) == 0) { - profile = argv[arg_index] + 10; - ++arg_index; - } else if (std::strcmp(argv[arg_index], "-p") == 0 && arg_index + 1 < argc) { - profile = argv[arg_index + 1]; - arg_index += 2; } else if (std::strcmp(argv[arg_index], "--") == 0) { ++arg_index; break; @@ -116,8 +109,6 @@ try { } } - (void)(profile); // TODO: profile support - argc -= arg_index; argv += arg_index; From 07231c16306e2f5f59f26b7f25f44a9affb72ce9 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 2 Aug 2014 21:33:37 -0700 Subject: [PATCH 40/47] Set 'required' option on Git filter to true This signals to Git that the filter must complete successfully for the content to be usable. --- commands.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/commands.cpp b/commands.cpp index 0e5f7df..3f8d489 100644 --- a/commands.cpp +++ b/commands.cpp @@ -72,11 +72,13 @@ static void configure_git_filters (const char* key_name) escaped_git_crypt_path + " smudge --key-name=" + key_name); git_config(std::string("filter.git-crypt-") + key_name + ".clean", escaped_git_crypt_path + " clean --key-name=" + key_name); + git_config(std::string("filter.git-crypt-") + key_name + ".required", "true"); git_config(std::string("diff.git-crypt-") + key_name + ".textconv", escaped_git_crypt_path + " diff --key-name=" + key_name); } else { git_config("filter.git-crypt.smudge", escaped_git_crypt_path + " smudge"); git_config("filter.git-crypt.clean", escaped_git_crypt_path + " clean"); + git_config("filter.git-crypt.required", "true"); git_config("diff.git-crypt.textconv", escaped_git_crypt_path + " diff"); } } From b07f49b9b37118dd6c8da909c53149b5e085cbf6 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Wed, 6 Aug 2014 19:02:42 -0700 Subject: [PATCH 41/47] smudge: if file is not encrypted, just copy through clear text Since Git consults the checked-out .gitattributes instead of the .gitattributes in effect at the time the file was committed, Git may invoke the smudge filter on old versions of a file that were committed without encryption. --- commands.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/commands.cpp b/commands.cpp index 3f8d489..2ee80e1 100644 --- a/commands.cpp +++ b/commands.cpp @@ -579,8 +579,11 @@ int smudge (int argc, const char** argv) unsigned char header[10 + Aes_ctr_decryptor::NONCE_LEN]; std::cin.read(reinterpret_cast(header), sizeof(header)); if (std::cin.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) { - std::clog << "git-crypt: error: file not encrypted" << std::endl; - return 1; + // File not encrypted - just copy it out to stdout + std::clog << "git-crypt: warning: file not encrypted" << std::endl; // TODO: display additional information explaining why file might be unencrypted + std::cout.write(reinterpret_cast(header), std::cin.gcount()); // include the bytes which we already read + std::cout << std::cin.rdbuf(); + return 0; } return decrypt_file_to_stdout(key_file, header, std::cin); @@ -619,7 +622,7 @@ int diff (int argc, const char** argv) in.read(reinterpret_cast(header), sizeof(header)); if (in.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) { // File not encrypted - just copy it out to stdout - std::cout.write(reinterpret_cast(header), in.gcount()); // don't forget to include the header which we read! + std::cout.write(reinterpret_cast(header), in.gcount()); // include the bytes which we already read std::cout << in.rdbuf(); return 0; } From 8b159b543f0105c88e46dd256604d21fdf784e12 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Mon, 18 Aug 2014 14:11:37 -0700 Subject: [PATCH 42/47] Avoid possible undefined behavior with empty std::vector In particular, &bytes[0] is undefined if bytes is empty. --- key.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/key.cpp b/key.cpp index 2da3b8d..6b0a512 100644 --- a/key.cpp +++ b/key.cpp @@ -209,12 +209,18 @@ void Key_file::load_header (std::istream& in) if (field_len > KEY_NAME_MAX_LEN) { throw Malformed(); } - std::vector bytes(field_len); - in.read(&bytes[0], field_len); - if (in.gcount() != static_cast(field_len)) { - throw Malformed(); + if (field_len == 0) { + // special case field_len==0 to avoid possible undefined behavior + // edge cases with an empty std::vector (particularly, &bytes[0]). + key_name.clear(); + } else { + std::vector bytes(field_len); + in.read(&bytes[0], field_len); + if (in.gcount() != static_cast(field_len)) { + throw Malformed(); + } + key_name.assign(&bytes[0], field_len); } - key_name.assign(&bytes[0], field_len); if (!validate_key_name(key_name.c_str())) { key_name.clear(); throw Malformed(); From f50feec2dd2ef8326b7a2bc19ed1181d8a45c248 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 6 Sep 2014 10:33:02 -0700 Subject: [PATCH 43/47] Display helpful information when smudge detects an unencrypted file --- commands.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/commands.cpp b/commands.cpp index 2ee80e1..9ebc8db 100644 --- a/commands.cpp +++ b/commands.cpp @@ -580,7 +580,12 @@ int smudge (int argc, const char** argv) std::cin.read(reinterpret_cast(header), sizeof(header)); if (std::cin.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) { // File not encrypted - just copy it out to stdout - std::clog << "git-crypt: warning: file not encrypted" << std::endl; // TODO: display additional information explaining why file might be unencrypted + std::clog << "git-crypt: Warning: file not encrypted" << std::endl; + std::clog << "git-crypt: Run 'git-crypt status' to make sure all files are properly encrypted." << std::endl; + std::clog << "git-crypt: If 'git-crypt status' reports no problems, then an older version of" << std::endl; + std::clog << "git-crypt: this file may be unencrypted in the repository's history. If this" << std::endl; + std::clog << "git-crypt: file contains sensitive information, you can use 'git filter-branch'" << std::endl; + std::clog << "git-crypt: to remove its old versions from the history." << std::endl; std::cout.write(reinterpret_cast(header), std::cin.gcount()); // include the bytes which we already read std::cout << std::cin.rdbuf(); return 0; From 10622f6dcc5d25a2e3e9b26a5113e7a165a2ff14 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 6 Sep 2014 14:59:16 -0700 Subject: [PATCH 44/47] Raise an error if legacy key file has trailing data --- key.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/key.cpp b/key.cpp index 6b0a512..d8fa482 100644 --- a/key.cpp +++ b/key.cpp @@ -117,6 +117,13 @@ void Key_file::Entry::load_legacy (uint32_t arg_version, std::istream& in) if (in.gcount() != HMAC_KEY_LEN) { throw Malformed(); } + + if (in.peek() != -1) { + // Trailing data is a good indication that we are not actually reading a + // legacy key file. (This is important to check since legacy key files + // did not have any sort of file header.) + throw Malformed(); + } } void Key_file::Entry::store (std::ostream& out) const From e37566f18022cda5c2917e3faa74c4ca409a43da Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 6 Sep 2014 15:43:00 -0700 Subject: [PATCH 45/47] status: properly detect files encrypted with alternative key names --- commands.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/commands.cpp b/commands.cpp index 9ebc8db..c9da12d 100644 --- a/commands.cpp +++ b/commands.cpp @@ -1147,7 +1147,7 @@ int status (int argc, const char** argv) // 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) const std::pair file_attrs(get_file_attributes(filename)); - if (file_attrs.first == "git-crypt") { // TODO: key_name support + if (file_attrs.first == "git-crypt" || std::strncmp(file_attrs.first.c_str(), "git-crypt-", 10) == 0) { // File is encrypted const bool blob_is_unencrypted = !object_id.empty() && !check_if_blob_is_encrypted(object_id); @@ -1174,6 +1174,7 @@ int status (int argc, const char** argv) } } } else if (!fix_problems && !show_unencrypted_only) { + // TODO: output the key name used to encrypt this file std::cout << " encrypted: " << filename; if (file_attrs.second != file_attrs.first) { // but diff filter is not properly set From adaea41d4e6d9f3b96086e416d2bf340e522cfca Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 6 Sep 2014 17:25:05 -0700 Subject: [PATCH 46/47] add-gpg-key: add -n/--no-commit option to inhibit committing --- commands.cpp | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/commands.cpp b/commands.cpp index c9da12d..c5b76ec 100644 --- a/commands.cpp +++ b/commands.cpp @@ -808,9 +808,12 @@ int unlock (int argc, const char** argv) int add_gpg_key (int argc, const char** argv) { const char* key_name = 0; + bool no_commit = 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)); int argi = parse_options(options, argc, argv); if (argc - argi == 0) { @@ -862,26 +865,27 @@ int add_gpg_key (int argc, const char** argv) } // git commit ... - // TODO: add a command line option (-n perhaps) to inhibit committing - // 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'; - } + if (!no_commit) { + // 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'; + } - // git commit -m MESSAGE NEW_FILE ... - command.clear(); - command.push_back("git"); - command.push_back("commit"); - command.push_back("-m"); - command.push_back(commit_message_builder.str()); - command.push_back("--"); - command.insert(command.end(), new_files.begin(), new_files.end()); + // git commit -m MESSAGE NEW_FILE ... + command.clear(); + command.push_back("git"); + command.push_back("commit"); + command.push_back("-m"); + command.push_back(commit_message_builder.str()); + command.push_back("--"); + command.insert(command.end(), new_files.begin(), new_files.end()); - if (!successful_exit(exec_command(command))) { - std::clog << "Error: 'git commit' failed" << std::endl; - return 1; + if (!successful_exit(exec_command(command))) { + std::clog << "Error: 'git commit' failed" << std::endl; + return 1; + } } } From 725f442ce4d085fc095f3000ac5a864db6c1c7c5 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 6 Sep 2014 17:25:31 -0700 Subject: [PATCH 47/47] Remove a TODO comment I've decided not to do it --- commands.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/commands.cpp b/commands.cpp index c5b76ec..636983a 100644 --- a/commands.cpp +++ b/commands.cpp @@ -719,7 +719,6 @@ int unlock (int argc, const char** argv) std::vector key_files; if (argc > 0) { // Read from the symmetric key file(s) - // TODO: command line flag to accept legacy key format? for (int argi = 0; argi < argc; ++argi) { const char* symmetric_key_file = argv[argi];