From 2f0216104280231cb02a807af2ec8d3047b0143f Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 22 Mar 2014 11:41:18 -0700 Subject: [PATCH 01/97] 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/97] 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/97] 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/97] 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/97] 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/97] 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/97] 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 5638aa0e0e6a036f69319a01aa1c1054d8a61f17 Mon Sep 17 00:00:00 2001 From: Adam Nelson Date: Mon, 31 Mar 2014 13:55:30 +0300 Subject: [PATCH 08/97] INSTALL updated for using homebrew on OS X --- INSTALL | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/INSTALL b/INSTALL index 0bd4da8..acdf9e3 100644 --- a/INSTALL +++ b/INSTALL @@ -20,3 +20,9 @@ The Makefile is tailored for g++, but should work with other compilers. It doesn't matter where you install the git-crypt binary - choose wherever is most convenient for you. + +INSTALLING ON OS X + +Using the brew package manager, simply run: + + $ brew install git-crypt From 7fe8443238c8d09e74f6fac6d294588a52057eb0 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Mon, 31 Mar 2014 21:31:33 -0700 Subject: [PATCH 09/97] Minor tidy-up of INSTALL file --- INSTALL | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/INSTALL b/INSTALL index acdf9e3..3dc4f26 100644 --- a/INSTALL +++ b/INSTALL @@ -21,7 +21,8 @@ The Makefile is tailored for g++, but should work with other compilers. It doesn't matter where you install the git-crypt binary - choose wherever is most convenient for you. -INSTALLING ON OS X + +INSTALLING ON MAC OS X Using the brew package manager, simply run: From 7b2604e79ef404bdf8ab28f2d8db1e6901f0fc49 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Tue, 1 Apr 2014 09:57:40 -0700 Subject: [PATCH 10/97] Document git-crypt's new mailing lists in the README --- README | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README b/README index d31ae8c..7e6961d 100644 --- a/README +++ b/README @@ -100,3 +100,12 @@ git-crypt does not itself provide any authentication. It assumes that either the master copy of your repository is stored securely, or that you are using git's existing facilities to ensure integrity (signed tags, remembering commit hashes, etc.). + + +MAILING LISTS + +To stay abreast of, and provide input to, git-crypt development, consider +subscribing to one or both of our mailing lists: + +Announcements: http://lists.cloudmutt.com/mailman/listinfo/git-crypt-announce +Discussion: http://lists.cloudmutt.com/mailman/listinfo/git-crypt-discuss From b3e843cfc4f65b9110adfe7507a6a345d84ee231 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Tue, 1 Apr 2014 16:15:57 -0700 Subject: [PATCH 11/97] Fix include guards to not start with _ Since such names are reserved, technically. --- commands.hpp | 4 ++-- crypto.hpp | 4 ++-- util.hpp | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/commands.hpp b/commands.hpp index ce68129..ccce978 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 void clean (const char* keyfile); diff --git a/crypto.hpp b/crypto.hpp index e8166e2..6bde6ab 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 #include diff --git a/util.hpp b/util.hpp index aa76982..18ea199 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 8c77209d40c5c0027041e3b6acbe55e5f7b5bbfe Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Tue, 1 Apr 2014 16:18:28 -0700 Subject: [PATCH 12/97] 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 d43a26ab0d776dbbc1c66177c1d1117d3337f05c Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Tue, 1 Apr 2014 21:54:45 -0700 Subject: [PATCH 13/97] Document Debian package building in INSTALL file --- INSTALL | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/INSTALL b/INSTALL index 3dc4f26..35e4156 100644 --- a/INSTALL +++ b/INSTALL @@ -22,6 +22,15 @@ It doesn't matter where you install the git-crypt binary - choose wherever is most convenient for you. +BUILDING A DEBIAN PACKAGE + +Debian packaging can be found in the 'debian' branch of the project +Git repository. The package is built using git-buildpackage as follows: + + $ git checkout debian + $ git-buildpackage -uc -us + + INSTALLING ON MAC OS X Using the brew package manager, simply run: From 1843104015c3b7ca15b93e48dc52b6b61a4dd30e Mon Sep 17 00:00:00 2001 From: Darayus Nanavati Date: Sun, 16 Jun 2013 22:20:03 +1000 Subject: [PATCH 14/97] convert documentation files to markdown * format section headings, links and code snippets * add .md file extension to trigger pretty rendering on Github * standardize on lowercase typesetting for git-crypt --- INSTALL.md | 40 ++++++++++++++++++ NEWS.md | 21 ++++++++++ README.md | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 INSTALL.md create mode 100644 NEWS.md create mode 100644 README.md diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..fc868f9 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,40 @@ +Dependencies +------------ + +To use git-crypt, you need: + +* Git 1.6.0 or newer +* OpenSSL +* For decrypted git diff output, Git 1.6.1 or newer +* For decrypted git blame output, Git 1.7.2 or newer + +To build git-crypt, you need a C++ compiler and OpenSSL development +headers. + + +Building GIT-CRYPT +------------------ + +The Makefile is tailored for g++, but should work with other compilers. + + make + cp git-crypt /usr/local/bin/ + +It doesn't matter where you install the git-crypt binary - choose wherever +is most convenient for you. + + +BUILDING A DEBIAN PACKAGE + +Debian packaging can be found in the 'debian' branch of the project +Git repository. The package is built using git-buildpackage as follows: + + $ git checkout debian + $ git-buildpackage -uc -us + + +INSTALLING ON MAC OS X + +Using the brew package manager, simply run: + + $ brew install git-crypt diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..062c7d5 --- /dev/null +++ b/NEWS.md @@ -0,0 +1,21 @@ +News +==== + +######v0.3 (2013-04-05) +* Fix 'git-crypt init' on newer versions of Git. Previously, + encrypted files were not being automatically decrypted after + running 'git-crypt init' with recent versions of Git. +* Allow 'git-crypt init' to be run even if the working tree contains + untracked files. +* 'git-crypt init' now properly escapes arguments to the filter + commands it configures, allowing both the path to git-crypt and the + path to the key file to contain arbitrary characters such as spaces. + +######v0.2 (2013-01-25) +* Numerous improvements to 'git-crypt init' usability. +* Fix gitattributes example in README: the old example showed a colon + after the filename where there shouldn't be one. +* Various build fixes and improvements. + +######v0.1 (2012-11-29) +* Initial release. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cb0086f --- /dev/null +++ b/README.md @@ -0,0 +1,116 @@ +GIT-CRYPT +========= + +git-crypt enables transparent encryption and decryption of files in a +git repository. Files which you choose to protect are encrypted when +committed, and decrypted when checked out. git-crypt lets you freely +share a repository containing a mix of public and private content. +git-crypt gracefully degrades, so developers without the secret key can +still clone and commit to a repository with encrypted files. This lets +you store your secret material (such as keys or passwords) in the same +repository as your code, without requiring you to lock down your entire +repository. + +git-crypt was written by Andrew Ayer . +For more information, see . + + +Building git-crypt +------------------ +See the INSTALL file. + + +Using git-crypt +--------------- + +Generate a secret key: + + git-crypt keygen /path/to/keyfile + +Configure a repository to use encryption: + + cd repo + git-crypt init /path/to/keyfile + +Specify files to encrypt by creating a .gitattributes file: + + secretfile filter=git-crypt diff=git-crypt + *.key filter=git-crypt diff=git-crypt + +Like a .gitignore file, it can match wildcards and should be checked +into the repository. Make sure you don't accidentally encrypt the +.gitattributes file itself! + +Cloning a repository with encrypted files: + + git clone /path/to/repo + cd repo + git-crypt init /path/to/keyfile + +That's all you need to do - after running `git-crypt init`, you can use +git normally - encryption and decryption happen transparently. + + +Current Status +-------------- + +The latest version of git-crypt is 0.3, released on 2013-04-05. +git-crypt aims to be bug-free and reliable, meaning it shouldn't +crash, malfunction, or expose your confidential data. However, +it has not yet reached maturity, meaning it is not as documented, +featureful, or easy-to-use as it should be. Additionally, there may be +backwards-incompatible changes introduced before version 1.0. + +Development on git-crypt is currently focused on improving the user +experience, especially around setting up repositories. There are also +plans to add additional key management schemes, such as passphrase-derived +keys and keys encrypted with PGP. + + +Security +-------- + +git-crypt is more secure that other transparent git encryption systems. +git-crypt encrypts files using AES-256 in CTR mode with a synthetic IV +derived from the SHA-1 HMAC of the file. This is provably semantically +secure under deterministic chosen-plaintext attack. That means that +although the encryption is deterministic (which is required so git can +distinguish when a file has and hasn't changed), it leaks no information +beyond whether two files are identical or not. Other proposals for +transparent git encryption use ECB or CBC with a fixed IV. These systems +are not semantically secure and leak information. + +The AES key is stored unencrypted on disk. The user is responsible for +protecting it and ensuring it's safely distributed only to authorized +people. A future version of git-crypt may support encrypting the key +with a passphrase. + + +Limitations +----------- + +git-crypt is not designed to encrypt an entire repository. Not only does +that defeat the aim of git-crypt, which is the ability to selectively +encrypt files and share the repository with less-trusted developers, there +are probably better, more efficient ways to encrypt an entire repository, +such as by storing it on an encrypted filesystem. Also note that +git-crypt is somewhat of an abuse of git's smudge, clean, and textconv +features. Junio Hamano, git's maintainer, has said not to do this +, +though his main objection ("making a pair of similar 'smudged' contents +totally dissimilar in their 'clean' counterparts.") does not apply here +since git-crypt uses deterministic encryption. + +git-crypt does not itself provide any authentication. It assumes that +either the master copy of your repository is stored securely, or that +you are using git's existing facilities to ensure integrity (signed tags, +remembering commit hashes, etc.). + + +MAILING LISTS + +To stay abreast of, and provide input to, git-crypt development, consider +subscribing to one or both of our mailing lists: + +Announcements: http://lists.cloudmutt.com/mailman/listinfo/git-crypt-announce +Discussion: http://lists.cloudmutt.com/mailman/listinfo/git-crypt-discuss From 29e589da3f0f8a9787e3a28addd83633b4546dca Mon Sep 17 00:00:00 2001 From: Darayus Nanavati Date: Wed, 28 May 2014 17:24:38 +0300 Subject: [PATCH 15/97] cross-link documentation files using Markdown relative links --- NEWS.md | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index 062c7d5..46f3a4a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -13,7 +13,7 @@ News ######v0.2 (2013-01-25) * Numerous improvements to 'git-crypt init' usability. -* Fix gitattributes example in README: the old example showed a colon +* Fix gitattributes example in [README](README.md): the old example showed a colon after the filename where there shouldn't be one. * Various build fixes and improvements. diff --git a/README.md b/README.md index cb0086f..d7e4a3d 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ For more information, see . Building git-crypt ------------------ -See the INSTALL file. +See the [INSTALL.md](INSTALL.md) file. Using git-crypt @@ -54,7 +54,7 @@ git normally - encryption and decryption happen transparently. Current Status -------------- -The latest version of git-crypt is 0.3, released on 2013-04-05. +The latest version of git-crypt is [0.3](NEWS.md), released on 2013-04-05. git-crypt aims to be bug-free and reliable, meaning it shouldn't crash, malfunction, or expose your confidential data. However, it has not yet reached maturity, meaning it is not as documented, From 79263fc57c65932c8d25710f18fd0f68f209c3a0 Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Wed, 28 May 2014 17:32:16 +0300 Subject: [PATCH 16/97] fix link and header formatting; re-wrap text --- INSTALL.md | 20 +++++++++-------- NEWS.md | 8 +++---- README.md | 66 ++++++++++++++++++++++++++---------------------------- 3 files changed, 47 insertions(+), 47 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index fc868f9..f756ca5 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -20,21 +20,23 @@ The Makefile is tailored for g++, but should work with other compilers. make cp git-crypt /usr/local/bin/ -It doesn't matter where you install the git-crypt binary - choose wherever -is most convenient for you. +It doesn't matter where you install the git-crypt binary - choose +wherever is most convenient for you. -BUILDING A DEBIAN PACKAGE +Building A Debian Package +------------------------- -Debian packaging can be found in the 'debian' branch of the project -Git repository. The package is built using git-buildpackage as follows: +Debian packaging can be found in the 'debian' branch of the project Git +repository. The package is built using git-buildpackage as follows: - $ git checkout debian - $ git-buildpackage -uc -us + git checkout debian + git-buildpackage -uc -us -INSTALLING ON MAC OS X +Installing On Mac OS X +---------------------- Using the brew package manager, simply run: - $ brew install git-crypt + brew install git-crypt diff --git a/NEWS.md b/NEWS.md index 46f3a4a..65b5b7b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,8 +3,8 @@ News ######v0.3 (2013-04-05) * Fix 'git-crypt init' on newer versions of Git. Previously, - encrypted files were not being automatically decrypted after - running 'git-crypt init' with recent versions of Git. + encrypted files were not being automatically decrypted after running + 'git-crypt init' with recent versions of Git. * Allow 'git-crypt init' to be run even if the working tree contains untracked files. * 'git-crypt init' now properly escapes arguments to the filter @@ -13,8 +13,8 @@ News ######v0.2 (2013-01-25) * Numerous improvements to 'git-crypt init' usability. -* Fix gitattributes example in [README](README.md): the old example showed a colon - after the filename where there shouldn't be one. +* Fix gitattributes example in [README](README.md): the old example + showed a colon after the filename where there shouldn't be one. * Various build fixes and improvements. ######v0.1 (2012-11-29) diff --git a/README.md b/README.md index d7e4a3d..4656e64 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,8 @@ you store your secret material (such as keys or passwords) in the same repository as your code, without requiring you to lock down your entire repository. -git-crypt was written by Andrew Ayer . -For more information, see . - +git-crypt was written by Andrew Ayer . For +more information, see . Building git-crypt ------------------ @@ -50,22 +49,21 @@ Cloning a repository with encrypted files: That's all you need to do - after running `git-crypt init`, you can use git normally - encryption and decryption happen transparently. - Current Status -------------- -The latest version of git-crypt is [0.3](NEWS.md), released on 2013-04-05. -git-crypt aims to be bug-free and reliable, meaning it shouldn't -crash, malfunction, or expose your confidential data. However, -it has not yet reached maturity, meaning it is not as documented, -featureful, or easy-to-use as it should be. Additionally, there may be -backwards-incompatible changes introduced before version 1.0. +The latest version of git-crypt is [0.3](NEWS.md), released on +2013-04-05. git-crypt aims to be bug-free and reliable, meaning it +shouldn't crash, malfunction, or expose your confidential data. +However, it has not yet reached maturity, meaning it is not as +documented, featureful, or easy-to-use as it should be. Additionally, +there may be backwards-incompatible changes introduced before version +1.0. Development on git-crypt is currently focused on improving the user experience, especially around setting up repositories. There are also -plans to add additional key management schemes, such as passphrase-derived -keys and keys encrypted with PGP. - +plans to add additional key management schemes, such as +passphrase-derived keys and keys encrypted with PGP. Security -------- @@ -77,40 +75,40 @@ secure under deterministic chosen-plaintext attack. That means that although the encryption is deterministic (which is required so git can distinguish when a file has and hasn't changed), it leaks no information beyond whether two files are identical or not. Other proposals for -transparent git encryption use ECB or CBC with a fixed IV. These systems -are not semantically secure and leak information. +transparent git encryption use ECB or CBC with a fixed IV. These +systems are not semantically secure and leak information. The AES key is stored unencrypted on disk. The user is responsible for protecting it and ensuring it's safely distributed only to authorized people. A future version of git-crypt may support encrypting the key with a passphrase. - Limitations ----------- -git-crypt is not designed to encrypt an entire repository. Not only does -that defeat the aim of git-crypt, which is the ability to selectively -encrypt files and share the repository with less-trusted developers, there -are probably better, more efficient ways to encrypt an entire repository, -such as by storing it on an encrypted filesystem. Also note that -git-crypt is somewhat of an abuse of git's smudge, clean, and textconv -features. Junio Hamano, git's maintainer, has said not to do this -, -though his main objection ("making a pair of similar 'smudged' contents -totally dissimilar in their 'clean' counterparts.") does not apply here -since git-crypt uses deterministic encryption. +git-crypt is not designed to encrypt an entire repository. Not only +does that defeat the aim of git-crypt, which is the ability to +selectively encrypt files and share the repository with less-trusted +developers, there are probably better, more efficient ways to encrypt an +entire repository, such as by storing it on an encrypted filesystem. +Also note that git-crypt is somewhat of an abuse of git's smudge, clean, +and textconv features. Junio Hamano, git's maintainer, has [said not to +do this][1], though his main objection ("making a pair of similar +'smudged' contents totally dissimilar in their 'clean' counterparts.") +does not apply here since git-crypt uses deterministic encryption. git-crypt does not itself provide any authentication. It assumes that either the master copy of your repository is stored securely, or that -you are using git's existing facilities to ensure integrity (signed tags, -remembering commit hashes, etc.). +you are using git's existing facilities to ensure integrity (signed +tags, remembering commit hashes, etc.). +Mailing Lists +------------- -MAILING LISTS +To stay abreast of, and provide input to, git-crypt development, +consider subscribing to one or both of our mailing lists: -To stay abreast of, and provide input to, git-crypt development, consider -subscribing to one or both of our mailing lists: +* [Announcements](http://lists.cloudmutt.com/mailman/listinfo/git-crypt-announce) +* [Discussion](http://lists.cloudmutt.com/mailman/listinfo/git-crypt-discuss) -Announcements: http://lists.cloudmutt.com/mailman/listinfo/git-crypt-announce -Discussion: http://lists.cloudmutt.com/mailman/listinfo/git-crypt-discuss + [1]: http://thread.gmane.org/gmane.comp.version-control.git/113124/focus=113221 From 22323bc3a5ba61d837aac71f1b469261b296e0ce Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Mon, 2 Jun 2014 17:12:59 -0700 Subject: [PATCH 17/97] In README, use HTTPS URI for git-crypt's website --- README | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README b/README index 7e6961d..b04ed64 100644 --- a/README +++ b/README @@ -11,7 +11,7 @@ repository as your code, without requiring you to lock down your entire repository. git-crypt was written by Andrew Ayer . -For more information, see . +For more information, see . BUILDING GIT-CRYPT diff --git a/README.md b/README.md index 4656e64..d8f440f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ repository as your code, without requiring you to lock down your entire repository. git-crypt was written by Andrew Ayer . For -more information, see . +more information, see . Building git-crypt ------------------ From 19ea278a31e58dc99da2301a08d2322fdabd0bf9 Mon Sep 17 00:00:00 2001 From: Simon Kotlinski Date: Tue, 3 Jun 2014 13:17:16 +0200 Subject: [PATCH 18/97] 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 f0b8e60..b7ddc8d 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 c2a9e48de5668cd59a2b0b54c0b819e0e9d58559 Mon Sep 17 00:00:00 2001 From: Simon Kotlinski Date: Tue, 3 Jun 2014 13:17:16 +0200 Subject: [PATCH 19/97] 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 20/97] 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 21/97] 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 22/97] 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 23/97] 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 24/97] 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 25/97] 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 26/97] 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 27/97] 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 28/97] 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 29/97] 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 30/97] 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 31/97] 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 32/97] 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 33/97] 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 34/97] 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 35/97] 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 36/97] 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 37/97] 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 38/97] 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 39/97] 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 40/97] 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 41/97] 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 42/97] 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 43/97] 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 44/97] 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 45/97] 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 46/97] 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 47/97] 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 48/97] 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 49/97] 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 50/97] 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 51/97] 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 52/97] 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 53/97] 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 54/97] 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 55/97] 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 56/97] 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 57/97] 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]; From 9c190a5a89903529ca9ac1548ae2e8a3764bb1d0 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 6 Sep 2014 19:33:54 -0700 Subject: [PATCH 58/97] Add CONTRIBUTING and THANKS files --- CONTRIBUTING.md | 25 +++++++++++++++++++++++++ THANKS.md | 18 ++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100644 THANKS.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8b4b482 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,25 @@ +Thanks for your interest in contributing to git-crypt! git-crypt is +open source software and welcomes contributions in the form of code, +documentation, bug reports, or anything else that improves git-crypt. + +When contributing code, please consider the following guidelines: + + * You are encouraged to open an issue on GitHub or send mail to + git-crypt-discuss@lists.cloudmutt.com to discuss any non-trivial + changes before you start coding. + + * Please mimic the existing code style as much as possible. In + particular, please indent code using tab characters with a width + of 8. + + * To minimize merge commits, please rebase your changes before opening + a pull request. + + * To submit your patch, open a pull request on GitHub or send a + properly-formatted patch to git-crypt-discuss@lists.cloudmutt.com. + +Finally, be aware that since git-crypt is security-sensitive software, +the bar for contributions is higher than average. Please don't be +discouraged by this, but be prepared for patches to possibly go through +several rounds of feedback and improvement before being accepted. +Your patience and understanding is appreciated. diff --git a/THANKS.md b/THANKS.md new file mode 100644 index 0000000..fa63b07 --- /dev/null +++ b/THANKS.md @@ -0,0 +1,18 @@ +For their contributions to git-crypt, I thank: + + * Michael Mior and @zimbatm for the Homebrew formula. + + * Cyril Cleaud for help with Windows support. + + * The following people for contributing patches: + * Adam Nelson + * Caleb Maclennan + * Darayus Nanavati + * Jon Sailor + * Linus G Thiel + * Simon Kotlinski + + * And everyone who has tested git-crypt, provided feedback, reported + bugs, and participated in discussions about new features. + +Thank you! From 4495af12744e031300d99deb9dd8233b8da8fde0 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sun, 7 Sep 2014 13:06:03 -0700 Subject: [PATCH 59/97] README: update security and limitations sections --- README | 45 +++++++++++++++++++++++++-------------------- README.md | 46 +++++++++++++++++++++++++--------------------- 2 files changed, 50 insertions(+), 41 deletions(-) diff --git a/README b/README index b04ed64..2cd9ad1 100644 --- a/README +++ b/README @@ -76,30 +76,35 @@ beyond whether two files are identical or not. Other proposals for transparent git encryption use ECB or CBC with a fixed IV. These systems are not semantically secure and leak information. -The AES key is stored unencrypted on disk. The user is responsible for -protecting it and ensuring it's safely distributed only to authorized -people. A future version of git-crypt may support encrypting the key -with a passphrase. - LIMITATIONS -git-crypt is not designed to encrypt an entire repository. Not only does -that defeat the aim of git-crypt, which is the ability to selectively -encrypt files and share the repository with less-trusted developers, there -are probably better, more efficient ways to encrypt an entire repository, -such as by storing it on an encrypted filesystem. Also note that -git-crypt is somewhat of an abuse of git's smudge, clean, and textconv -features. Junio Hamano, git's maintainer, has said not to do this -, -though his main objection ("making a pair of similar 'smudged' contents -totally dissimilar in their 'clean' counterparts.") does not apply here -since git-crypt uses deterministic encryption. +git-crypt relies on git filters, which were not designed with encryption +in mind. As such, git-crypt is not the best tool for encrypting most or +all of the files in a repository. Where git-crypt really shines is where +most of your repository is public, but you have a few files (perhaps +private keys named *.key, or a file with API credentials) which you +need to encrypt. For encrypting an entire repository, consider using a +system like git-remote-gcrypt +instead. (Note: no endorsement is made of git-remote-gcrypt's security.) -git-crypt does not itself provide any authentication. It assumes that -either the master copy of your repository is stored securely, or that -you are using git's existing facilities to ensure integrity (signed tags, -remembering commit hashes, etc.). +git-crypt does not encrypt file names, commit messages, or other metadata. + +Files encrypted with git-crypt are not compressible. Even the smallest +change to an encrypted file requires git to store the entire changed file, +instead of just a delta. + +Files encrypted with git-crypt cannot be patched with git-apply, unless +the patch itself is encrypted. To generate an encrypted patch, use `git +diff --no-textconv --binary`. Alternatively, you can apply a plaintext +patch outside of git using the patch command. + +Although git-crypt protects individual file contents with a SHA-1 +HMAC, git-crypt cannot be used securely unless the entire repository is +protected against tampering (an attacker who can mutate your repository +can alter your .gitattributes file to disable encryption). If necessary, +use git features such as signed tags instead of relying solely on +git-crypt for integrity. MAILING LISTS diff --git a/README.md b/README.md index d8f440f..04e3cce 100644 --- a/README.md +++ b/README.md @@ -78,29 +78,35 @@ beyond whether two files are identical or not. Other proposals for transparent git encryption use ECB or CBC with a fixed IV. These systems are not semantically secure and leak information. -The AES key is stored unencrypted on disk. The user is responsible for -protecting it and ensuring it's safely distributed only to authorized -people. A future version of git-crypt may support encrypting the key -with a passphrase. - Limitations ----------- -git-crypt is not designed to encrypt an entire repository. Not only -does that defeat the aim of git-crypt, which is the ability to -selectively encrypt files and share the repository with less-trusted -developers, there are probably better, more efficient ways to encrypt an -entire repository, such as by storing it on an encrypted filesystem. -Also note that git-crypt is somewhat of an abuse of git's smudge, clean, -and textconv features. Junio Hamano, git's maintainer, has [said not to -do this][1], though his main objection ("making a pair of similar -'smudged' contents totally dissimilar in their 'clean' counterparts.") -does not apply here since git-crypt uses deterministic encryption. +git-crypt relies on git filters, which were not designed with encryption +in mind. As such, git-crypt is not the best tool for encrypting most or +all of the files in a repository. Where git-crypt really shines is where +most of your repository is public, but you have a few files (perhaps +private keys named *.key, or a file with API credentials) which you +need to encrypt. For encrypting an entire repository, consider using a +system like [git-remote-gcrypt](https://github.com/joeyh/git-remote-gcrypt) +instead. (Note: no endorsement is made of git-remote-gcrypt's security.) -git-crypt does not itself provide any authentication. It assumes that -either the master copy of your repository is stored securely, or that -you are using git's existing facilities to ensure integrity (signed -tags, remembering commit hashes, etc.). +git-crypt does not encrypt file names, commit messages, or other metadata. + +Files encrypted with git-crypt are not compressible. Even the smallest +change to an encrypted file requires git to store the entire changed file, +instead of just a delta. + +Files encrypted with git-crypt cannot be patched with git-apply, unless +the patch itself is encrypted. To generate an encrypted patch, use `git +diff --no-textconv --binary`. Alternatively, you can apply a plaintext +patch outside of git using the patch command. + +Although git-crypt protects individual file contents with a SHA-1 +HMAC, git-crypt cannot be used securely unless the entire repository is +protected against tampering (an attacker who can mutate your repository +can alter your .gitattributes file to disable encryption). If necessary, +use git features such as signed tags instead of relying solely on +git-crypt for integrity. Mailing Lists ------------- @@ -110,5 +116,3 @@ consider subscribing to one or both of our mailing lists: * [Announcements](http://lists.cloudmutt.com/mailman/listinfo/git-crypt-announce) * [Discussion](http://lists.cloudmutt.com/mailman/listinfo/git-crypt-discuss) - - [1]: http://thread.gmane.org/gmane.comp.version-control.git/113124/focus=113221 From 8460d00bbf007686a21318bb3254f665a9e98e2c Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sun, 7 Sep 2014 13:31:57 -0700 Subject: [PATCH 60/97] README: add notes about gitattributes --- README | 26 +++++++++++++++++++++++--- README.md | 26 +++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/README b/README index 2cd9ad1..8dfad4d 100644 --- a/README +++ b/README @@ -35,9 +35,10 @@ Specify files to encrypt by creating a .gitattributes file: secretfile filter=git-crypt diff=git-crypt *.key filter=git-crypt diff=git-crypt -Like a .gitignore file, it can match wildcards and should be checked -into the repository. Make sure you don't accidentally encrypt the -.gitattributes file itself! +Like a .gitignore file, it can match wildcards and should be checked into +the repository. See below for more information about .gitattributes +files. Make sure you don't accidentally encrypt the .gitattributes +file itself! Cloning a repository with encrypted files: @@ -107,6 +108,25 @@ use git features such as signed tags instead of relying solely on git-crypt for integrity. +GITATTRIBUTES FILE + +The .gitattributes file is documented in the gitattributes(5) man page. +The file pattern format is the same as the one used by .gitignore, +as documented in the gitignore(5) man page, with the exception that +specifying a directory name in .gitattributes is not sufficient to +encrypt all files beneath it. + +Also note that the pattern `dir/*` does not match files under +sub-directories of dir/. To encrypt an entire sub-tree dir/, place the +following in dir/.gitattributes: + + * filter=git-crypt diff=git-crypt + .gitattributes !filter !diff + +The second pattern is essential for ensuring that .gitattributes itself +is not encrypted. + + MAILING LISTS To stay abreast of, and provide input to, git-crypt development, consider diff --git a/README.md b/README.md index 04e3cce..83ea481 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,10 @@ Specify files to encrypt by creating a .gitattributes file: secretfile filter=git-crypt diff=git-crypt *.key filter=git-crypt diff=git-crypt -Like a .gitignore file, it can match wildcards and should be checked -into the repository. Make sure you don't accidentally encrypt the -.gitattributes file itself! +Like a .gitignore file, it can match wildcards and should be checked into +the repository. See below for more information about .gitattributes +files. Make sure you don't accidentally encrypt the .gitattributes +file itself! Cloning a repository with encrypted files: @@ -108,6 +109,25 @@ can alter your .gitattributes file to disable encryption). If necessary, use git features such as signed tags instead of relying solely on git-crypt for integrity. +Gitattributes File +------------------ + +The .gitattributes file is documented in the gitattributes(5) man page. +The file pattern format is the same as the one used by .gitignore, +as documented in the gitignore(5) man page, with the exception that +specifying a directory name in .gitattributes is not sufficient to +encrypt all files beneath it. + +Also note that the pattern `dir/*` does not match files under +sub-directories of dir/. To encrypt an entire sub-tree dir/, place the +following in dir/.gitattributes: + + * filter=git-crypt diff=git-crypt + .gitattributes !filter !diff + +The second pattern is essential for ensuring that .gitattributes itself +is not encrypted. + Mailing Lists ------------- From 316e194f846e119fcf049838205b85831cf8444c Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Tue, 9 Sep 2014 08:57:39 -0700 Subject: [PATCH 61/97] README: document problems with Atlassian SourceTree --- README | 4 ++++ README.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/README b/README index 8dfad4d..48111f5 100644 --- a/README +++ b/README @@ -107,6 +107,10 @@ can alter your .gitattributes file to disable encryption). If necessary, use git features such as signed tags instead of relying solely on git-crypt for integrity. +git-crypt does not work reliably with Atlassian SourceTree. +Files might be left in an unencrypted state. See +. + GITATTRIBUTES FILE diff --git a/README.md b/README.md index 83ea481..732f62a 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,10 @@ can alter your .gitattributes file to disable encryption). If necessary, use git features such as signed tags instead of relying solely on git-crypt for integrity. +git-crypt does [not work reliably with Atlassian +SourceTree](https://jira.atlassian.com/browse/SRCTREE-2511). Files might +be left in an unencrypted state. + Gitattributes File ------------------ From 3726df181d2e28f8ad57f88e4df6ed603b06a8b6 Mon Sep 17 00:00:00 2001 From: Michael Schout Date: Thu, 4 Sep 2014 16:23:34 -0500 Subject: [PATCH 62/97] add support for "git-crypt lock" This does the reverse of what git-crypt unlock does: - unconfigures the git filters - forcibly checks out HEAD version Usage: git crypt lock locks repo using the "default" key git crypt lock -k NAME locks the repo, using unlocked key named NAME git crypt lock --key-name=NAME git crypt lock -a locks the repo, removing ALL unlocked keys git crypt lock --all Result is that you can now decrypt and then revert back to encrypted form of files and vice versa. Modified-by: Andrew Ayer * Make argv argument to lock() const. * Minor whitespace/style fixes to conform to project conventions. Signed-off-by: Andrew Ayer --- commands.cpp | 163 ++++++++++++++++++++++++++++++++++++++++++++------ commands.hpp | 1 + git-crypt.cpp | 4 ++ 3 files changed, 150 insertions(+), 18 deletions(-) diff --git a/commands.cpp b/commands.cpp index 636983a..d3c5069 100644 --- a/commands.cpp +++ b/commands.cpp @@ -62,6 +62,19 @@ static void git_config (const std::string& name, const std::string& value) } } +static void git_unconfig (const std::string& name) +{ + std::vector command; + command.push_back("git"); + command.push_back("config"); + command.push_back("--remove-section"); + command.push_back(name); + + if (!successful_exit(exec_command(command))) { + throw Error("'git config' failed"); + } +} + static void configure_git_filters (const char* key_name) { std::string escaped_git_crypt_path(escape_shell_arg(our_exe_path())); @@ -83,6 +96,43 @@ static void configure_git_filters (const char* key_name) } } +static void unconfigure_git_filters (const char* key_name) +{ + // unconfigure the git-crypt filters + if (key_name && (strncmp(key_name, "default", 7) != 0)) { + // named key + git_unconfig(std::string("filter.git-crypt-") + key_name); + git_unconfig(std::string("diff.git-crypt-") + key_name); + } else { + // default key + git_unconfig("filter.git-crypt"); + git_unconfig("diff.git-crypt"); + } +} + +static bool git_checkout_head (const std::string& top_dir) +{ + std::vector command; + + command.push_back("git"); + command.push_back("checkout"); + command.push_back("-f"); + command.push_back("HEAD"); + command.push_back("--"); + + if (top_dir.empty()) { + command.push_back("."); + } else { + command.push_back(top_dir); + } + + if (!successful_exit(exec_command(command))) { + return false; + } + + return true; +} + static bool same_key_name (const char* a, const char* b) { return (!a && !b) || (a && b && std::strcmp(a, b) == 0); @@ -96,7 +146,7 @@ static void validate_key_name_or_throw (const char* key_name) } } -static std::string get_internal_key_path (const char* key_name) +static std::string get_internal_keys_path () { // git rev-parse --git-dir std::vector command; @@ -112,8 +162,17 @@ static std::string get_internal_key_path (const char* key_name) std::string path; std::getline(output, path); - path += "/git-crypt/keys/"; + path += "/git-crypt/keys"; + + return path; +} + +static std::string get_internal_key_path (const char* key_name) +{ + std::string path(get_internal_keys_path()); + path += "/"; path += key_name ? key_name : "default"; + return path; } @@ -313,6 +372,15 @@ static void load_key (Key_file& key_file, const char* key_name, const char* key_ } } +static void unlink_repo_key (const char* key_name) +{ + std::string key_path(get_internal_key_path(key_name ? key_name : "default")); + + if ((unlink(key_path.c_str())) == -1 && errno != ENOENT) { + throw System_error("Unable to remove repo key", key_path, errno); + } +} + 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) { @@ -401,8 +469,6 @@ static int parse_plumbing_options (const char** key_name, const char** key_file, return parse_options(options, argc, argv); } - - // Encrypt contents of stdin and write to stdout int clean (int argc, const char** argv) { @@ -781,20 +847,7 @@ int unlock (int argc, const char** argv) // If HEAD doesn't exist (perhaps because this repo doesn't have any files yet) // just skip the checkout. if (head_exists) { - // 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.push_back("."); - } else { - command.push_back(path_to_top); - } - - if (!successful_exit(exec_command(command))) { + if (!git_checkout_head(path_to_top)) { 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; @@ -804,6 +857,80 @@ int unlock (int argc, const char** argv) return 0; } +int lock (int argc, const char** argv) +{ + const char* key_name = 0; + bool all_keys = false; + Options_list options; + options.push_back(Option_def("-k", &key_name)); + options.push_back(Option_def("--key-name", &key_name)); + options.push_back(Option_def("-a", &all_keys)); + options.push_back(Option_def("--all", &all_keys)); + + int argi = parse_options(options, argc, argv); + + if (argc - argi != 0) { + std::clog << "Usage: git-crypt lock [-k KEYNAME] [--all]" << 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 + // untracked files so it's safe to ignore those. + + // Running 'git status' also serves as a check that the Git repo is accessible. + + std::stringstream status_output; + 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. + std::clog << "Error: Working directory not clean." << std::endl; + std::clog << "Please commit your changes or 'git stash' them before running 'git-crypt' lock." << std::endl; + return 1; + } + + // 2. Determine the path to the top of the repository. We pass this as the argument + // to 'git checkout' below. (Determine the path now so in case it fails we haven't already + // mucked with the git config.) + std::string path_to_top(get_path_to_top()); + + // 3. unconfigure the git filters and remove decrypted keys + if (all_keys) { + // unconfigure for all keys + std::vector dirents = get_directory_contents(get_internal_keys_path().c_str()); + + for (std::vector::const_iterator dirent(dirents.begin()); dirent != dirents.end(); ++dirent) { + unlink_repo_key(dirent->c_str()); + unconfigure_git_filters(dirent->c_str()); + } + } else { + // just handle the given key + unlink_repo_key(key_name); + unconfigure_git_filters(key_name); + } + + // 4. Do a force checkout so any files that were previously checked out decrypted + // will now be checked out encrypted. + // If HEAD doesn't exist (perhaps because this repo doesn't have any files yet) + // just skip the checkout. + if (head_exists) { + if (!git_checkout_head(path_to_top)) { + std::clog << "Error: 'git checkout' failed" << std::endl; + std::clog << "git-crypt has been locked but up but existing decrypted files have not been encrypted" << std::endl; + return 1; + } + } + + return 0; +} + int add_gpg_key (int argc, const char** argv) { const char* key_name = 0; diff --git a/commands.hpp b/commands.hpp index 2575ca6..abb936d 100644 --- a/commands.hpp +++ b/commands.hpp @@ -46,6 +46,7 @@ int diff (int argc, const char** argv); // Public commands: int init (int argc, const char** argv); int unlock (int argc, const char** argv); +int lock (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); diff --git a/git-crypt.cpp b/git-crypt.cpp index d0567ab..8f2ef90 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -57,6 +57,7 @@ static void print_usage (std::ostream& out) //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 << " lock check out encrypted versions of files in this repo" << 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; @@ -135,6 +136,9 @@ try { if (std::strcmp(command, "unlock") == 0) { return unlock(argc, argv); } + if (std::strcmp(command, "lock") == 0) { + return lock(argc, argv); + } if (std::strcmp(command, "add-gpg-key") == 0) { return add_gpg_key(argc, argv); } From 42aa7db245c7c6290446c0cd89214771eecf3c0c Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Fri, 12 Sep 2014 19:39:34 -0700 Subject: [PATCH 63/97] Credit Michael Schout in THANKS file --- THANKS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/THANKS.md b/THANKS.md index fa63b07..4c0d53e 100644 --- a/THANKS.md +++ b/THANKS.md @@ -10,6 +10,7 @@ For their contributions to git-crypt, I thank: * Darayus Nanavati * Jon Sailor * Linus G Thiel + * Michael Schout * Simon Kotlinski * And everyone who has tested git-crypt, provided feedback, reported From 70879eaf57f6c71e5cb5b056e2dd7cd36864a93f Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Fri, 12 Sep 2014 19:39:15 -0700 Subject: [PATCH 64/97] Tweak git-crypt usage message * Change the wording for 'git-crypt lock'. * Move 'git-crypt lock' to 'Common commands' section since it's common to both GPG and symmetric mode. * Reduce whitespace in the output so it fits in 80 characters. --- git-crypt.cpp | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/git-crypt.cpp b/git-crypt.cpp index 8f2ef90..8bbe823 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -48,25 +48,25 @@ static void print_usage (std::ostream& out) 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 << " 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 << " lock de-configure git-crypt and re-encrypt files in working tree" << std::endl; out << std::endl; out << "GPG commands:" << 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 << " lock check out encrypted versions of files in this repo" << 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 << " 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 << " 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; From 690dba2f14de98d2013651d9946ddca392caa68d Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Fri, 12 Sep 2014 19:41:54 -0700 Subject: [PATCH 65/97] Add multi-platform remove_file helper And use it for deleting internal keys --- commands.cpp | 12 ++++-------- util-unix.cpp | 7 +++++++ util-win32.cpp | 7 +++++++ util.hpp | 1 + 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/commands.cpp b/commands.cpp index d3c5069..4b593f6 100644 --- a/commands.cpp +++ b/commands.cpp @@ -372,13 +372,9 @@ static void load_key (Key_file& key_file, const char* key_name, const char* key_ } } -static void unlink_repo_key (const char* key_name) +static void unlink_internal_key (const char* key_name) { - std::string key_path(get_internal_key_path(key_name ? key_name : "default")); - - if ((unlink(key_path.c_str())) == -1 && errno != ENOENT) { - throw System_error("Unable to remove repo key", key_path, errno); - } + remove_file(get_internal_key_path(key_name ? key_name : "default")); } 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) @@ -907,12 +903,12 @@ int lock (int argc, const char** argv) std::vector dirents = get_directory_contents(get_internal_keys_path().c_str()); for (std::vector::const_iterator dirent(dirents.begin()); dirent != dirents.end(); ++dirent) { - unlink_repo_key(dirent->c_str()); + unlink_internal_key(dirent->c_str()); unconfigure_git_filters(dirent->c_str()); } } else { // just handle the given key - unlink_repo_key(key_name); + unlink_internal_key(key_name); unconfigure_git_filters(key_name); } diff --git a/util-unix.cpp b/util-unix.cpp index 2bdf364..d31550b 100644 --- a/util-unix.cpp +++ b/util-unix.cpp @@ -285,6 +285,13 @@ void touch_file (const std::string& filename) } } +void remove_file (const std::string& filename) +{ + if (unlink(filename.c_str()) == -1) { + throw System_error("unlink", filename, errno); + } +} + static void init_std_streams_platform () { } diff --git a/util-win32.cpp b/util-win32.cpp index 4d442db..21576c7 100644 --- a/util-win32.cpp +++ b/util-win32.cpp @@ -340,6 +340,13 @@ void touch_file (const std::string& filename) CloseHandle(fh); } +void remove_file (const std::string& filename) +{ + if (!DeleteFileA(filename.c_str())) { + throw System_error("DeleteFileA", filename, GetLastError()); + } +} + static void init_std_streams_platform () { _setmode(_fileno(stdin), _O_BINARY); diff --git a/util.hpp b/util.hpp index aa04912..8b5bc33 100644 --- a/util.hpp +++ b/util.hpp @@ -65,6 +65,7 @@ int exec_command (const std::vector&, std::ostream& output); int exec_command_with_input (const std::vector&, const char* p, size_t len); bool successful_exit (int status); void touch_file (const std::string&); +void remove_file (const std::string&); std::string escape_shell_arg (const std::string&); uint32_t load_be32 (const unsigned char*); void store_be32 (unsigned char*, uint32_t); From 88e8e3a265dbbd0fbda00b6c546f53a6de1d9717 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Fri, 12 Sep 2014 19:42:59 -0700 Subject: [PATCH 66/97] Display error if both -k and -a options passed to `git-crypt lock` --- commands.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/commands.cpp b/commands.cpp index 4b593f6..3a6b0c1 100644 --- a/commands.cpp +++ b/commands.cpp @@ -870,6 +870,11 @@ int lock (int argc, const char** argv) return 2; } + if (all_keys && key_name) { + std::clog << "Error: -k and --all options are mutually exclusive" << 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 From e9e90fc873068d856789f0ccf16b26a115ecf9dc Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Fri, 12 Sep 2014 19:45:21 -0700 Subject: [PATCH 67/97] For consistency, always use NULL internally to represent the default key --- commands.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/commands.cpp b/commands.cpp index 3a6b0c1..f7ab302 100644 --- a/commands.cpp +++ b/commands.cpp @@ -99,7 +99,7 @@ static void configure_git_filters (const char* key_name) static void unconfigure_git_filters (const char* key_name) { // unconfigure the git-crypt filters - if (key_name && (strncmp(key_name, "default", 7) != 0)) { + if (key_name) { // named key git_unconfig(std::string("filter.git-crypt-") + key_name); git_unconfig(std::string("diff.git-crypt-") + key_name); @@ -908,8 +908,9 @@ int lock (int argc, const char** argv) std::vector dirents = get_directory_contents(get_internal_keys_path().c_str()); for (std::vector::const_iterator dirent(dirents.begin()); dirent != dirents.end(); ++dirent) { - unlink_internal_key(dirent->c_str()); - unconfigure_git_filters(dirent->c_str()); + const char* this_key_name = (*dirent == "default" ? 0 : dirent->c_str()); + unlink_internal_key(this_key_name); + unconfigure_git_filters(this_key_name); } } else { // just handle the given key From 3799a23aa7b0e731cdcfb28ddb5a6474727cfe78 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Fri, 12 Sep 2014 19:40:42 -0700 Subject: [PATCH 68/97] Add missing argument when throwing System_error --- util-unix.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util-unix.cpp b/util-unix.cpp index d31550b..1cebf3f 100644 --- a/util-unix.cpp +++ b/util-unix.cpp @@ -281,7 +281,7 @@ bool successful_exit (int status) void touch_file (const std::string& filename) { if (utimes(filename.c_str(), NULL) == -1) { - throw System_error("utimes", "", errno); + throw System_error("utimes", filename, errno); } } From 16c4a827c0693c3a65b355eb4a15c77f13c88c68 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Fri, 12 Sep 2014 19:55:19 -0700 Subject: [PATCH 69/97] Error message if you try to lock repository that's not locked --- commands.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/commands.cpp b/commands.cpp index f7ab302..da6df44 100644 --- a/commands.cpp +++ b/commands.cpp @@ -914,6 +914,15 @@ int lock (int argc, const char** argv) } } else { // just handle the given key + if (access(get_internal_key_path(key_name).c_str(), F_OK) == -1 && errno == ENOENT) { + std::clog << "Error: this repository is not currently locked"; + if (key_name) { + std::clog << " with key '" << key_name << "'"; + } + std::clog << "." << std::endl; + return 1; + } + unlink_internal_key(key_name); unconfigure_git_filters(key_name); } From 746bb5def3149a6053dd804e74e9151e1ec29819 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Fri, 12 Sep 2014 19:56:40 -0700 Subject: [PATCH 70/97] Remove unlink_internal_key function I think it's simpler this way. --- commands.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/commands.cpp b/commands.cpp index da6df44..083d033 100644 --- a/commands.cpp +++ b/commands.cpp @@ -372,11 +372,6 @@ static void load_key (Key_file& key_file, const char* key_name, const char* key_ } } -static void unlink_internal_key (const char* key_name) -{ - remove_file(get_internal_key_path(key_name ? key_name : "default")); -} - 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) { @@ -909,12 +904,13 @@ int lock (int argc, const char** argv) for (std::vector::const_iterator dirent(dirents.begin()); dirent != dirents.end(); ++dirent) { const char* this_key_name = (*dirent == "default" ? 0 : dirent->c_str()); - unlink_internal_key(this_key_name); + remove_file(get_internal_key_path(this_key_name)); unconfigure_git_filters(this_key_name); } } else { // just handle the given key - if (access(get_internal_key_path(key_name).c_str(), F_OK) == -1 && errno == ENOENT) { + std::string internal_key_path(get_internal_key_path(key_name)); + if (access(internal_key_path.c_str(), F_OK) == -1 && errno == ENOENT) { std::clog << "Error: this repository is not currently locked"; if (key_name) { std::clog << " with key '" << key_name << "'"; @@ -923,7 +919,7 @@ int lock (int argc, const char** argv) return 1; } - unlink_internal_key(key_name); + remove_file(internal_key_path); unconfigure_git_filters(key_name); } From 0538d111fc284413b04876be9c4aea1044a62fae Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 13 Sep 2014 11:24:32 -0700 Subject: [PATCH 71/97] Usage message: refer to gpg key argument as "user ID" not "key ID" This is the terminology that the gpg man page uses. --- git-crypt.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git-crypt.cpp b/git-crypt.cpp index 8bbe823..c69bef7 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -54,8 +54,8 @@ static void print_usage (std::ostream& out) out << " lock de-configure git-crypt and re-encrypt files in working tree" << std::endl; out << std::endl; out << "GPG commands:" << 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 << " add-gpg-key USRID add the user with the given GPG user ID as a collaborator" << std::endl; + //out << " rm-gpg-key USRID revoke collaborator status from the given GPG user 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; From 9e340b510d777a004a8109e7fe002cb2a5da1cb2 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 13 Sep 2014 11:25:44 -0700 Subject: [PATCH 72/97] Document GPG mode in README --- README | 44 +++++++++++++++++++++++++++++--------------- README.md | 44 +++++++++++++++++++++++++++++--------------- 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/README b/README index 48111f5..cad34f6 100644 --- a/README +++ b/README @@ -21,14 +21,10 @@ See the INSTALL file. USING GIT-CRYPT -Generate a secret key: - - $ git-crypt keygen /path/to/keyfile - -Configure a repository to use encryption: +Configure a repository to use git-crypt: $ cd repo - $ git-crypt init /path/to/keyfile + $ git-crypt init Specify files to encrypt by creating a .gitattributes file: @@ -36,18 +32,36 @@ Specify files to encrypt by creating a .gitattributes file: *.key filter=git-crypt diff=git-crypt Like a .gitignore file, it can match wildcards and should be checked into -the repository. See below for more information about .gitattributes -files. Make sure you don't accidentally encrypt the .gitattributes -file itself! +the repository. See below for more information about .gitattributes. +Make sure you don't accidentally encrypt the .gitattributes file itself! -Cloning a repository with encrypted files: +Share the repository with others (or with yourself) using GPG: - $ git clone /path/to/repo - $ cd repo - $ git-crypt init /path/to/keyfile + $ git-crypt add-gpg-key USER_ID -That's all you need to do - after running git-crypt init, you can use -git normally - encryption and decryption happen transparently. +USER_ID can be a key ID, a full fingerprint, an email address, or anything +else that uniquely identifies a key to GPG (see "HOW TO SPECIFY A USER +ID" in the gpg man page). Note: `git-crypt add-gpg-key` will add and +commit a GPG-encrypted key file in the .git-crypt directory of the root +of your repository. + +Alternatively, you can export a symmetric secret key, which you must +securely convey to collaborators (GPG is not required, and no files +are added to your repository): + + $ git-crypt export-key /path/to/key + +After cloning a repository with encrypted files, unlock with with GPG: + + $ git-crypt unlock + +Or with a symmetric key: + + $ git-crypt unlock /path/to/key + +That's all you need to do - after git-crypt is set up (either with +`git-crypt init` or `git-crypt unlock`), you can use git normally - +encryption and decryption happen transparently. CURRENT STATUS diff --git a/README.md b/README.md index 732f62a..67fafa7 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,10 @@ See the [INSTALL.md](INSTALL.md) file. Using git-crypt --------------- -Generate a secret key: - - git-crypt keygen /path/to/keyfile - -Configure a repository to use encryption: +Configure a repository to use git-crypt: cd repo - git-crypt init /path/to/keyfile + git-crypt init Specify files to encrypt by creating a .gitattributes file: @@ -37,18 +33,36 @@ Specify files to encrypt by creating a .gitattributes file: *.key filter=git-crypt diff=git-crypt Like a .gitignore file, it can match wildcards and should be checked into -the repository. See below for more information about .gitattributes -files. Make sure you don't accidentally encrypt the .gitattributes -file itself! +the repository. See below for more information about .gitattributes. +Make sure you don't accidentally encrypt the .gitattributes file itself! -Cloning a repository with encrypted files: +Share the repository with others (or with yourself) using GPG: - git clone /path/to/repo - cd repo - git-crypt init /path/to/keyfile + git-crypt add-gpg-key USER_ID -That's all you need to do - after running `git-crypt init`, you can use -git normally - encryption and decryption happen transparently. +`USER_ID` can be a key ID, a full fingerprint, an email address, or anything +else that uniquely identifies a key to GPG (see "HOW TO SPECIFY A USER +ID" in the gpg man page). Note: `git-crypt add-gpg-key` will add and +commit a GPG-encrypted key file in the .git-crypt directory of the root +of your repository. + +Alternatively, you can export a symmetric secret key, which you must +securely convey to collaborators (GPG is not required, and no files +are added to your repository): + + git-crypt export-key /path/to/key + +After cloning a repository with encrypted files, unlock with with GPG: + + git-crypt unlock + +Or with a symmetric key: + + git-crypt unlock /path/to/key + +That's all you need to do - after git-crypt is set up (either with +`git-crypt init` or `git-crypt unlock`), you can use git normally - +encryption and decryption happen transparently. Current Status -------------- From acc3d2ecb3526dd194ea9a34be811414e749c8cb Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 13 Sep 2014 11:33:54 -0700 Subject: [PATCH 73/97] Fix capitalization of git-crypt in INSTALL.md --- INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/INSTALL.md b/INSTALL.md index f756ca5..5e78caa 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -12,7 +12,7 @@ To build git-crypt, you need a C++ compiler and OpenSSL development headers. -Building GIT-CRYPT +Building git-crypt ------------------ The Makefile is tailored for g++, but should work with other compilers. From 24fff1ce6fd99534c6ebf52fdda8a7f055a233c6 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 13 Sep 2014 11:34:14 -0700 Subject: [PATCH 74/97] Document experimental Windows support --- INSTALL | 11 +++++++++++ INSTALL.md | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/INSTALL b/INSTALL index 35e4156..a3e48c8 100644 --- a/INSTALL +++ b/INSTALL @@ -36,3 +36,14 @@ INSTALLING ON MAC OS X Using the brew package manager, simply run: $ brew install git-crypt + + +EXPERIMENTAL WINDOWS SUPPORT + +git-crypt should build on Windows with MinGW, although the build system +is not yet finalized so you will need to pass your own CXX, CXXFLAGS, and +LDFLAGS variables to make. Additionally, Windows support is less tested +and does not currently create key files with restrictive permissions, +making it unsuitable for use on a multi-user system. Windows support +will mature in a future version of git-crypt. Bug reports and patches +are most welcome! diff --git a/INSTALL.md b/INSTALL.md index 5e78caa..e348047 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -40,3 +40,14 @@ Installing On Mac OS X Using the brew package manager, simply run: brew install git-crypt + +Experimental Windows Support +---------------------------- + +git-crypt should build on Windows with MinGW, although the build system +is not yet finalized so you will need to pass your own CXX, CXXFLAGS, and +LDFLAGS variables to make. Additionally, Windows support is less tested +and does not currently create key files with restrictive permissions, +making it unsuitable for use on a multi-user system. Windows support +will mature in a future version of git-crypt. Bug reports and patches +are most welcome! From 04906c5355f860089c5f4fc9844ed728f16f1812 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sun, 14 Sep 2014 14:52:36 -0700 Subject: [PATCH 75/97] Write a usage message for every command You can run 'git-crypt help COMMAND' to see it. --- commands.cpp | 109 +++++++++++++++++++++++++++++++++--- commands.hpp | 15 ++++- git-crypt.cpp | 149 ++++++++++++++++++++++++++++++++------------------ 3 files changed, 211 insertions(+), 62 deletions(-) diff --git a/commands.cpp b/commands.cpp index 083d033..47fda6f 100644 --- a/commands.cpp +++ b/commands.cpp @@ -693,6 +693,15 @@ int diff (int argc, const char** argv) return decrypt_file_to_stdout(key_file, header, in); } +void help_init (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt init [OPTIONS]" << std::endl; + out << std::endl; + out << " -k, --key-name KEYNAME Initialize the given key, instead of the default" << std::endl; + out << std::endl; +} + int init (int argc, const char** argv) { const char* key_name = 0; @@ -709,7 +718,8 @@ int init (int argc, const char** argv) return unlock(argc, argv); } if (argc - argi != 0) { - std::clog << "Usage: git-crypt init [-k KEYNAME]" << std::endl; + std::clog << "Error: git-crypt init takes no arguments" << std::endl; + help_init(std::clog); return 2; } @@ -743,6 +753,12 @@ int init (int argc, const char** argv) return 0; } +void help_unlock (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt unlock" << std::endl; + out << " or: git-crypt unlock KEY_FILE ..." << std::endl; +} int unlock (int argc, const char** argv) { // 0. Make sure working directory is clean (ignoring untracked files) @@ -848,6 +864,15 @@ int unlock (int argc, const char** argv) return 0; } +void help_lock (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt lock [OPTIONS]" << std::endl; + out << std::endl; + out << " -a, --all Lock all keys, instead of just the default" << std::endl; + out << " -k, --key-name KEYNAME Lock the given key, instead of the default" << std::endl; + out << std::endl; +} int lock (int argc, const char** argv) { const char* key_name = 0; @@ -861,7 +886,8 @@ int lock (int argc, const char** argv) int argi = parse_options(options, argc, argv); if (argc - argi != 0) { - std::clog << "Usage: git-crypt lock [-k KEYNAME] [--all]" << std::endl; + std::clog << "Error: git-crypt lock takes no arguments" << std::endl; + help_lock(std::clog); return 2; } @@ -938,6 +964,15 @@ int lock (int argc, const char** argv) return 0; } +void help_add_gpg_key (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt add-gpg-key [OPTIONS] GPG_USER_ID ..." << std::endl; + out << std::endl; + out << " -k, --key-name KEYNAME Add GPG user to given key, instead of default" << std::endl; + out << " -n, --no-commit Don't automatically commit" << std::endl; + out << std::endl; +} int add_gpg_key (int argc, const char** argv) { const char* key_name = 0; @@ -950,7 +985,8 @@ int add_gpg_key (int argc, const char** argv) 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; + std::clog << "Error: no GPG user ID specified" << std::endl; + help_add_gpg_key(std::clog); return 2; } @@ -1025,12 +1061,26 @@ int add_gpg_key (int argc, const char** argv) return 0; } +void help_rm_gpg_key (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt rm-gpg-key [OPTIONS] GPG_USER_ID ..." << std::endl; + out << std::endl; + out << " -k, --key-name KEYNAME Remove user from given key, instead of default" << std::endl; + out << " -n, --no-commit Don't automatically commit" << std::endl; + out << std::endl; +} int rm_gpg_key (int argc, const char** argv) // TODO { std::clog << "Error: rm-gpg-key is not yet implemented." << std::endl; return 1; } +void help_ls_gpg_keys (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt ls-gpg-keys" << std::endl; +} int ls_gpg_keys (int argc, const char** argv) // TODO { // Sketch: @@ -1051,6 +1101,15 @@ int ls_gpg_keys (int argc, const char** argv) // TODO return 1; } +void help_export_key (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt export-key [OPTIONS] FILENAME" << std::endl; + out << std::endl; + out << " -k, --key-name KEYNAME Export the given key, instead of the default" << std::endl; + out << std::endl; + out << "When FILENAME is -, export to standard out." << std::endl; +} int export_key (int argc, const char** argv) { // TODO: provide options to export only certain key versions @@ -1062,7 +1121,8 @@ int export_key (int argc, const char** argv) int argi = parse_options(options, argc, argv); if (argc - argi != 1) { - std::clog << "Usage: git-crypt export-key [-k KEYNAME] FILENAME" << std::endl; + std::clog << "Error: no filename specified" << std::endl; + help_export_key(std::clog); return 2; } @@ -1083,10 +1143,18 @@ int export_key (int argc, const char** argv) return 0; } +void help_keygen (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt keygen FILENAME" << std::endl; + out << std::endl; + out << "When FILENAME is -, write to standard out." << std::endl; +} int keygen (int argc, const char** argv) { if (argc != 1) { - std::clog << "Usage: git-crypt keygen KEYFILE" << std::endl; + std::clog << "Error: no filename specified" << std::endl; + help_keygen(std::clog); return 2; } @@ -1112,10 +1180,18 @@ int keygen (int argc, const char** argv) return 0; } +void help_migrate_key (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt migrate-key FILENAME" << std::endl; + out << std::endl; + out << "When FILENAME is -, read from standard in and write to standard out." << std::endl; +} int migrate_key (int argc, const char** argv) { if (argc != 1) { - std::clog << "Usage: git-crypt migrate-key KEYFILE" << std::endl; + std::clog << "Error: no filename specified" << std::endl; + help_migrate_key(std::clog); return 2; } @@ -1162,12 +1238,31 @@ int migrate_key (int argc, const char** argv) return 0; } +void help_refresh (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt refresh" << std::endl; +} 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; } +void help_status (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt status [OPTIONS] [FILE ...]" << std::endl; + //out << " or: git-crypt status -r [OPTIONS]" << std::endl; + //out << " or: git-crypt status -f" << std::endl; + out << std::endl; + out << " -e Show encrypted files only" << std::endl; + out << " -u Show unencrypted files only" << std::endl; + //out << " -r Show repository status only" << std::endl; + out << " -f, --fix Fix problems with the repository" << std::endl; + //out << " -z Machine-parseable output" << std::endl; + out << std::endl; +} int status (int argc, const char** argv) { // Usage: @@ -1175,8 +1270,6 @@ int status (int argc, const char** argv) // git-crypt status [-e | -u] [-z] [FILE ...] Show encrypted status of files // git-crypt status -f Fix unencrypted blobs - // TODO: help option / usage output - 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 diff --git a/commands.hpp b/commands.hpp index abb936d..2bc603c 100644 --- a/commands.hpp +++ b/commands.hpp @@ -32,6 +32,7 @@ #define GIT_CRYPT_COMMANDS_HPP #include +#include struct Error { std::string message; @@ -56,5 +57,17 @@ int migrate_key (int argc, const char** argv); int refresh (int argc, const char** argv); int status (int argc, const char** argv); -#endif +// Help messages: +void help_init (std::ostream&); +void help_unlock (std::ostream&); +void help_lock (std::ostream&); +void help_add_gpg_key (std::ostream&); +void help_rm_gpg_key (std::ostream&); +void help_ls_gpg_keys (std::ostream&); +void help_export_key (std::ostream&); +void help_keygen (std::ostream&); +void help_migrate_key (std::ostream&); +void help_refresh (std::ostream&); +void help_status (std::ostream&); +#endif diff --git a/git-crypt.cpp b/git-crypt.cpp index c69bef7..ab107af 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -74,10 +74,51 @@ static void print_usage (std::ostream& out) 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; - */ +} + +static bool help_for_command (const char* command, std::ostream& out) +{ + if (std::strcmp(command, "init") == 0) { + help_init(out); + } else if (std::strcmp(command, "unlock") == 0) { + help_unlock(out); + } else if (std::strcmp(command, "lock") == 0) { + help_lock(out); + } else if (std::strcmp(command, "add-gpg-key") == 0) { + help_add_gpg_key(out); + } else if (std::strcmp(command, "rm-gpg-key") == 0) { + help_rm_gpg_key(out); + } else if (std::strcmp(command, "ls-gpg-keys") == 0) { + help_ls_gpg_keys(out); + } else if (std::strcmp(command, "export-key") == 0) { + help_export_key(out); + } else if (std::strcmp(command, "keygen") == 0) { + help_keygen(out); + } else if (std::strcmp(command, "migrate-key") == 0) { + help_migrate_key(out); + } else if (std::strcmp(command, "refresh") == 0) { + help_refresh(out); + } else if (std::strcmp(command, "status") == 0) { + help_status(out); + } else { + return false; + } + return true; +} + +static int help (int argc, const char** argv) +{ + if (argc == 0) { + print_usage(std::cout); + } else { + if (!help_for_command(argv[0], std::cout)) { + std::clog << "Error: '" << argv[0] << "' is not a git-crypt command. See 'git-crypt help'." << std::endl; + return 1; + } + } + return 0; } @@ -125,56 +166,61 @@ try { --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, "lock") == 0) { - return lock(argc, argv); - } - if (std::strcmp(command, "add-gpg-key") == 0) { - return add_gpg_key(argc, argv); - } - if (std::strcmp(command, "rm-gpg-key") == 0) { - return rm_gpg_key(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); - } - 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); - } - 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); - } - if (std::strcmp(command, "smudge") == 0) { - return smudge(argc, argv); - } - if (std::strcmp(command, "diff") == 0) { - return diff(argc, argv); + try { + // Public commands: + if (std::strcmp(command, "help") == 0) { + return help(argc, argv); + } + if (std::strcmp(command, "init") == 0) { + return init(argc, argv); + } + if (std::strcmp(command, "unlock") == 0) { + return unlock(argc, argv); + } + if (std::strcmp(command, "lock") == 0) { + return lock(argc, argv); + } + if (std::strcmp(command, "add-gpg-key") == 0) { + return add_gpg_key(argc, argv); + } + if (std::strcmp(command, "rm-gpg-key") == 0) { + return rm_gpg_key(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); + } + 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); + } + 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); + } + if (std::strcmp(command, "smudge") == 0) { + return smudge(argc, argv); + } + if (std::strcmp(command, "diff") == 0) { + return diff(argc, argv); + } + } catch (const Option_error& e) { + std::clog << "git-crypt: Error: " << e.option_name << ": " << e.message << std::endl; + help_for_command(command, std::clog); + return 2; } - print_usage(std::clog); + std::clog << "Error: '" << command << "' is not a git-crypt command. See 'git-crypt help'." << std::endl; return 2; } catch (const Error& e) { @@ -189,9 +235,6 @@ 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; From e4d1091e97dd992164028958888f500358875aa6 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Mon, 15 Sep 2014 10:02:22 -0700 Subject: [PATCH 76/97] Rename add-gpg-key command, etc. to add-gpg-user, etc. While writing the documention, I found that "GPG user" was less confusing terminology than "GPG key," since you aren't really adding a "key" to git-crypt, and git-crypt already uses "key" to refer to other concepts (cf. the -k/--key-name options). --- README | 10 +++++----- README.md | 12 ++++++------ commands.cpp | 24 ++++++++++++------------ commands.hpp | 12 ++++++------ git-crypt.cpp | 30 +++++++++++++++--------------- 5 files changed, 44 insertions(+), 44 deletions(-) diff --git a/README b/README index cad34f6..58d5454 100644 --- a/README +++ b/README @@ -37,13 +37,13 @@ Make sure you don't accidentally encrypt the .gitattributes file itself! Share the repository with others (or with yourself) using GPG: - $ git-crypt add-gpg-key USER_ID + $ git-crypt add-gpg-user USER_ID USER_ID can be a key ID, a full fingerprint, an email address, or anything -else that uniquely identifies a key to GPG (see "HOW TO SPECIFY A USER -ID" in the gpg man page). Note: `git-crypt add-gpg-key` will add and -commit a GPG-encrypted key file in the .git-crypt directory of the root -of your repository. +else that uniquely identifies a public key to GPG (see "HOW TO SPECIFY +A USER ID" in the gpg man page). Note: `git-crypt add-gpg-user` will +add and commit a GPG-encrypted key file in the .git-crypt directory of +the root of your repository. Alternatively, you can export a symmetric secret key, which you must securely convey to collaborators (GPG is not required, and no files diff --git a/README.md b/README.md index 67fafa7..15e5e6f 100644 --- a/README.md +++ b/README.md @@ -38,13 +38,13 @@ Make sure you don't accidentally encrypt the .gitattributes file itself! Share the repository with others (or with yourself) using GPG: - git-crypt add-gpg-key USER_ID + git-crypt add-gpg-user USER_ID -`USER_ID` can be a key ID, a full fingerprint, an email address, or anything -else that uniquely identifies a key to GPG (see "HOW TO SPECIFY A USER -ID" in the gpg man page). Note: `git-crypt add-gpg-key` will add and -commit a GPG-encrypted key file in the .git-crypt directory of the root -of your repository. +`USER_ID` can be a key ID, a full fingerprint, an email address, or +anything else that uniquely identifies a public key to GPG (see "HOW TO +SPECIFY A USER ID" in the gpg man page). Note: `git-crypt add-gpg-user` +will add and commit a GPG-encrypted key file in the .git-crypt directory +of the root of your repository. Alternatively, you can export a symmetric secret key, which you must securely convey to collaborators (GPG is not required, and no files diff --git a/commands.cpp b/commands.cpp index 47fda6f..22c2337 100644 --- a/commands.cpp +++ b/commands.cpp @@ -964,16 +964,16 @@ int lock (int argc, const char** argv) return 0; } -void help_add_gpg_key (std::ostream& out) +void help_add_gpg_user (std::ostream& out) { // |--------------------------------------------------------------------------------| 80 chars - out << "Usage: git-crypt add-gpg-key [OPTIONS] GPG_USER_ID ..." << std::endl; + out << "Usage: git-crypt add-gpg-user [OPTIONS] GPG_USER_ID ..." << std::endl; out << std::endl; out << " -k, --key-name KEYNAME Add GPG user to given key, instead of default" << std::endl; out << " -n, --no-commit Don't automatically commit" << std::endl; out << std::endl; } -int add_gpg_key (int argc, const char** argv) +int add_gpg_user (int argc, const char** argv) { const char* key_name = 0; bool no_commit = false; @@ -986,7 +986,7 @@ int add_gpg_key (int argc, const char** argv) int argi = parse_options(options, argc, argv); if (argc - argi == 0) { std::clog << "Error: no GPG user ID specified" << std::endl; - help_add_gpg_key(std::clog); + help_add_gpg_user(std::clog); return 2; } @@ -1061,27 +1061,27 @@ int add_gpg_key (int argc, const char** argv) return 0; } -void help_rm_gpg_key (std::ostream& out) +void help_rm_gpg_user (std::ostream& out) { // |--------------------------------------------------------------------------------| 80 chars - out << "Usage: git-crypt rm-gpg-key [OPTIONS] GPG_USER_ID ..." << std::endl; + out << "Usage: git-crypt rm-gpg-user [OPTIONS] GPG_USER_ID ..." << std::endl; out << std::endl; out << " -k, --key-name KEYNAME Remove user from given key, instead of default" << std::endl; out << " -n, --no-commit Don't automatically commit" << std::endl; out << std::endl; } -int rm_gpg_key (int argc, const char** argv) // TODO +int rm_gpg_user (int argc, const char** argv) // TODO { - std::clog << "Error: rm-gpg-key is not yet implemented." << std::endl; + std::clog << "Error: rm-gpg-user is not yet implemented." << std::endl; return 1; } -void help_ls_gpg_keys (std::ostream& out) +void help_ls_gpg_users (std::ostream& out) { // |--------------------------------------------------------------------------------| 80 chars - out << "Usage: git-crypt ls-gpg-keys" << std::endl; + out << "Usage: git-crypt ls-gpg-users" << std::endl; } -int ls_gpg_keys (int argc, const char** argv) // TODO +int ls_gpg_users (int argc, const char** argv) // TODO { // Sketch: // Scan the sub-directories in .git-crypt/keys, outputting something like this: @@ -1097,7 +1097,7 @@ int ls_gpg_keys (int argc, const 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-gpg-keys is not yet implemented." << std::endl; + std::clog << "Error: ls-gpg-users is not yet implemented." << std::endl; return 1; } diff --git a/commands.hpp b/commands.hpp index 2bc603c..32caa0f 100644 --- a/commands.hpp +++ b/commands.hpp @@ -48,9 +48,9 @@ int diff (int argc, const char** argv); int init (int argc, const char** argv); int unlock (int argc, const char** argv); int lock (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 add_gpg_user (int argc, const char** argv); +int rm_gpg_user (int argc, const char** argv); +int ls_gpg_users (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); @@ -61,9 +61,9 @@ int status (int argc, const char** argv); void help_init (std::ostream&); void help_unlock (std::ostream&); void help_lock (std::ostream&); -void help_add_gpg_key (std::ostream&); -void help_rm_gpg_key (std::ostream&); -void help_ls_gpg_keys (std::ostream&); +void help_add_gpg_user (std::ostream&); +void help_rm_gpg_user (std::ostream&); +void help_ls_gpg_users (std::ostream&); void help_export_key (std::ostream&); void help_keygen (std::ostream&); void help_migrate_key (std::ostream&); diff --git a/git-crypt.cpp b/git-crypt.cpp index ab107af..c424866 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -54,9 +54,9 @@ static void print_usage (std::ostream& out) out << " lock de-configure git-crypt and re-encrypt files in working tree" << std::endl; out << std::endl; out << "GPG commands:" << std::endl; - out << " add-gpg-key USRID add the user with the given GPG user ID as a collaborator" << std::endl; - //out << " rm-gpg-key USRID revoke collaborator status from the given GPG user ID" << std::endl; - //out << " ls-gpg-keys list the GPG key IDs of collaborators" << std::endl; + out << " add-gpg-user ID add the user with the given GPG user ID as a collaborator" << std::endl; + //out << " rm-gpg-user ID revoke collaborator status from the given GPG user ID" << std::endl; + //out << " ls-gpg-users 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; @@ -86,12 +86,12 @@ static bool help_for_command (const char* command, std::ostream& out) help_unlock(out); } else if (std::strcmp(command, "lock") == 0) { help_lock(out); - } else if (std::strcmp(command, "add-gpg-key") == 0) { - help_add_gpg_key(out); - } else if (std::strcmp(command, "rm-gpg-key") == 0) { - help_rm_gpg_key(out); - } else if (std::strcmp(command, "ls-gpg-keys") == 0) { - help_ls_gpg_keys(out); + } else if (std::strcmp(command, "add-gpg-user") == 0) { + help_add_gpg_user(out); + } else if (std::strcmp(command, "rm-gpg-user") == 0) { + help_rm_gpg_user(out); + } else if (std::strcmp(command, "ls-gpg-users") == 0) { + help_ls_gpg_users(out); } else if (std::strcmp(command, "export-key") == 0) { help_export_key(out); } else if (std::strcmp(command, "keygen") == 0) { @@ -180,14 +180,14 @@ try { if (std::strcmp(command, "lock") == 0) { return lock(argc, argv); } - if (std::strcmp(command, "add-gpg-key") == 0) { - return add_gpg_key(argc, argv); + if (std::strcmp(command, "add-gpg-user") == 0) { + return add_gpg_user(argc, argv); } - if (std::strcmp(command, "rm-gpg-key") == 0) { - return rm_gpg_key(argc, argv); + if (std::strcmp(command, "rm-gpg-user") == 0) { + return rm_gpg_user(argc, argv); } - if (std::strcmp(command, "ls-gpg-keys") == 0) { - return ls_gpg_keys(argc, argv); + if (std::strcmp(command, "ls-gpg-users") == 0) { + return ls_gpg_users(argc, argv); } if (std::strcmp(command, "export-key") == 0) { return export_key(argc, argv); From 4796a1e288cbf69e19a783ad5b3d7dbae4da47df Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sun, 21 Sep 2014 11:52:06 -0700 Subject: [PATCH 77/97] Clarify some wording in README --- README | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README b/README index 58d5454..a969d9e 100644 --- a/README +++ b/README @@ -131,7 +131,7 @@ GITATTRIBUTES FILE The .gitattributes file is documented in the gitattributes(5) man page. The file pattern format is the same as the one used by .gitignore, as documented in the gitignore(5) man page, with the exception that -specifying a directory name in .gitattributes is not sufficient to +specifying merely a directory (e.g. `/dir/`) is NOT sufficient to encrypt all files beneath it. Also note that the pattern `dir/*` does not match files under diff --git a/README.md b/README.md index 15e5e6f..7271f39 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ Gitattributes File The .gitattributes file is documented in the gitattributes(5) man page. The file pattern format is the same as the one used by .gitignore, as documented in the gitignore(5) man page, with the exception that -specifying a directory name in .gitattributes is not sufficient to +specifying merely a directory (e.g. `/dir/`) is *not* sufficient to encrypt all files beneath it. Also note that the pattern `dir/*` does not match files under From e70d067b485dc600e80b42e426f78fa4fa29546a Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sun, 21 Sep 2014 12:00:19 -0700 Subject: [PATCH 78/97] Rearrange a couple paragraphs in the README --- README | 10 +++++----- README.md | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README b/README index a969d9e..f9e7427 100644 --- a/README +++ b/README @@ -109,11 +109,6 @@ Files encrypted with git-crypt are not compressible. Even the smallest change to an encrypted file requires git to store the entire changed file, instead of just a delta. -Files encrypted with git-crypt cannot be patched with git-apply, unless -the patch itself is encrypted. To generate an encrypted patch, use `git -diff --no-textconv --binary`. Alternatively, you can apply a plaintext -patch outside of git using the patch command. - Although git-crypt protects individual file contents with a SHA-1 HMAC, git-crypt cannot be used securely unless the entire repository is protected against tampering (an attacker who can mutate your repository @@ -121,6 +116,11 @@ can alter your .gitattributes file to disable encryption). If necessary, use git features such as signed tags instead of relying solely on git-crypt for integrity. +Files encrypted with git-crypt cannot be patched with git-apply, unless +the patch itself is encrypted. To generate an encrypted patch, use `git +diff --no-textconv --binary`. Alternatively, you can apply a plaintext +patch outside of git using the patch command. + git-crypt does not work reliably with Atlassian SourceTree. Files might be left in an unencrypted state. See . diff --git a/README.md b/README.md index 7271f39..5a5d28e 100644 --- a/README.md +++ b/README.md @@ -111,11 +111,6 @@ Files encrypted with git-crypt are not compressible. Even the smallest change to an encrypted file requires git to store the entire changed file, instead of just a delta. -Files encrypted with git-crypt cannot be patched with git-apply, unless -the patch itself is encrypted. To generate an encrypted patch, use `git -diff --no-textconv --binary`. Alternatively, you can apply a plaintext -patch outside of git using the patch command. - Although git-crypt protects individual file contents with a SHA-1 HMAC, git-crypt cannot be used securely unless the entire repository is protected against tampering (an attacker who can mutate your repository @@ -123,6 +118,11 @@ can alter your .gitattributes file to disable encryption). If necessary, use git features such as signed tags instead of relying solely on git-crypt for integrity. +Files encrypted with git-crypt cannot be patched with git-apply, unless +the patch itself is encrypted. To generate an encrypted patch, use `git +diff --no-textconv --binary`. Alternatively, you can apply a plaintext +patch outside of git using the patch command. + git-crypt does [not work reliably with Atlassian SourceTree](https://jira.atlassian.com/browse/SRCTREE-2511). Files might be left in an unencrypted state. From cf990dc9dfe05b268f5d9f2f1e5244d2e7571fc8 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 15 Nov 2014 18:06:23 -0800 Subject: [PATCH 79/97] Minor formatting updates to README --- README | 4 ++-- README.md | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README b/README index f9e7427..297f855 100644 --- a/README +++ b/README @@ -10,8 +10,8 @@ you store your secret material (such as keys or passwords) in the same repository as your code, without requiring you to lock down your entire repository. -git-crypt was written by Andrew Ayer . -For more information, see . +git-crypt was written by Andrew Ayer . For more +information, see . BUILDING GIT-CRYPT diff --git a/README.md b/README.md index 5a5d28e..7a4db96 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -GIT-CRYPT -========= +git-crypt - transparent file encryption in git +============================================== git-crypt enables transparent encryption and decryption of files in a git repository. Files which you choose to protect are encrypted when @@ -11,8 +11,8 @@ you store your secret material (such as keys or passwords) in the same repository as your code, without requiring you to lock down your entire repository. -git-crypt was written by Andrew Ayer . For -more information, see . +git-crypt was written by [Andrew Ayer](https://www.agwa.name) (agwa@andrewayer.name). +For more information, see . Building git-crypt ------------------ From bd262f61265cfb80cb3d847bcdb115eea8a6e74d Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 15 Nov 2014 18:06:36 -0800 Subject: [PATCH 80/97] Add documentation for multiple keys --- doc/multiple_keys.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 doc/multiple_keys.md diff --git a/doc/multiple_keys.md b/doc/multiple_keys.md new file mode 100644 index 0000000..6d7fc69 --- /dev/null +++ b/doc/multiple_keys.md @@ -0,0 +1,24 @@ +In addition to the implicit default key, git-crypt supports alternative +keys which can be used to encrypt specific files and can be shared with +specific GPG users. This is useful if you want to grant different +collaborators access to different sets of files. + +To generate an alternative key named KEYNAME, pass the `-k KEYNAME` +option to `git-crypt init` as follows: + + git-crypt init -k KEYNAME + +To encrypt a file with an alternative key, use the `git-crypt-KEYNAME` +filter in `.gitattributes` as follows: + + secretfile filter=git-crypt-KEYNAME diff=git-crypt-KEYNAME + +To export an alternative key or share it with a GPG user, pass the `-k +KEYNAME` option to `git-crypt export-key` or `git-crypt add-gpg-user` +as follows: + + git-crypt export-key -k KEYNAME /path/to/keyfile + git-crypt add-gpg-user -k KEYNAME GPG_USER_ID + +To unlock a repository with an alternative key, use `git-crypt unlock` +normally. git-crypt will automatically determine which key is being used. From 3bf7d8e5121ea6e7e393760a00a835037ae93dea Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 15 Nov 2014 20:30:35 -0800 Subject: [PATCH 81/97] migrate-key: take separate arguments for old key and new key I don't want to encourage people to overwrite their old keys until they've successfully unlocked their repository with the migrated key. --- commands.cpp | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/commands.cpp b/commands.cpp index 22c2337..6d993ce 100644 --- a/commands.cpp +++ b/commands.cpp @@ -813,7 +813,7 @@ int unlock (int argc, const char** argv) } 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; + std::clog << "by running 'git-crypt migrate-key /path/to/old_key /path/to/migrated_key'." << std::endl; return 1; } @@ -1183,25 +1183,25 @@ int keygen (int argc, const char** argv) void help_migrate_key (std::ostream& out) { // |--------------------------------------------------------------------------------| 80 chars - out << "Usage: git-crypt migrate-key FILENAME" << std::endl; + out << "Usage: git-crypt migrate-key OLDFILENAME NEWFILENAME" << std::endl; out << std::endl; - out << "When FILENAME is -, read from standard in and write to standard out." << std::endl; + out << "Use - to read from standard in/write to standard out." << std::endl; } int migrate_key (int argc, const char** argv) { - if (argc != 1) { - std::clog << "Error: no filename specified" << std::endl; + if (argc != 2) { + std::clog << "Error: filenames not specified" << std::endl; help_migrate_key(std::clog); return 2; } const char* key_file_name = argv[0]; + const char* new_key_file_name = argv[1]; 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) { @@ -1209,26 +1209,15 @@ int migrate_key (int argc, const char** argv) 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_to_file(new_key_file_name.c_str())) { + if (std::strcmp(new_key_file_name, "-") == 0) { + key_file.store(std::cout); + } else { + if (!key_file.store_to_file(new_key_file_name)) { std::clog << "Error: " << new_key_file_name << ": unable to write key file" << std::endl; return 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; - } } } catch (Key_file::Malformed) { std::clog << "Error: " << key_file_name << ": not a valid legacy git-crypt key file" << std::endl; From 6520746bced495a51405d5320f1dec44f89bcc31 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 15 Nov 2014 20:31:01 -0800 Subject: [PATCH 82/97] Update 'git-crypt help' message Documented new arguments to 'git-crypt migrate-key' and adjusted spacing. --- git-crypt.cpp | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/git-crypt.cpp b/git-crypt.cpp index c424866..816bf2e 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -48,25 +48,25 @@ static void print_usage (std::ostream& out) 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 << " lock de-configure git-crypt and re-encrypt files in working tree" << 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 << " lock de-configure git-crypt and re-encrypt files in work tree" << std::endl; out << std::endl; out << "GPG commands:" << std::endl; - out << " add-gpg-user ID add the user with the given GPG user ID as a collaborator" << std::endl; - //out << " rm-gpg-user ID revoke collaborator status from the given GPG user ID" << std::endl; - //out << " ls-gpg-users list the GPG key IDs of collaborators" << std::endl; - out << " unlock decrypt this repo using the in-repo GPG-encrypted key" << std::endl; + out << " add-gpg-user USERID add the user with the given GPG user ID as a collaborator" << std::endl; + //out << " rm-gpg-user USERID revoke collaborator status from the given GPG user ID" << std::endl; + //out << " ls-gpg-users 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 << " 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 << " 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 OLD NEW migrate the legacy key file OLD to the new format in NEW" << std::endl; /* out << std::endl; out << "Plumbing commands (not to be used directly):" << std::endl; From be237fe27d7490cbbc1f9450bfefc0e0abf42aed Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sun, 16 Nov 2014 17:25:02 -0800 Subject: [PATCH 83/97] Fix formatting in NEWS.md --- NEWS.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/NEWS.md b/NEWS.md index 65b5b7b..ce434e7 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,17 +2,17 @@ News ==== ######v0.3 (2013-04-05) -* Fix 'git-crypt init' on newer versions of Git. Previously, +* Fix `git-crypt init` on newer versions of Git. Previously, encrypted files were not being automatically decrypted after running - 'git-crypt init' with recent versions of Git. -* Allow 'git-crypt init' to be run even if the working tree contains + `git-crypt init` with recent versions of Git. +* Allow `git-crypt init` to be run even if the working tree contains untracked files. -* 'git-crypt init' now properly escapes arguments to the filter +* `git-crypt init` now properly escapes arguments to the filter commands it configures, allowing both the path to git-crypt and the path to the key file to contain arbitrary characters such as spaces. ######v0.2 (2013-01-25) -* Numerous improvements to 'git-crypt init' usability. +* Numerous improvements to `git-crypt init` usability. * Fix gitattributes example in [README](README.md): the old example showed a colon after the filename where there shouldn't be one. * Various build fixes and improvements. From 3d53bce1a8262cf5b129d3a0ff6c671d949b5282 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sun, 16 Nov 2014 17:28:41 -0800 Subject: [PATCH 84/97] Add .gitattributes file to ignore .git files when creating archive --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e4504cf --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.git* export-ignore From 2b0cc1b5acc5241591d99ba10b05270577b76bcf Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sun, 16 Nov 2014 17:29:17 -0800 Subject: [PATCH 85/97] Update README, NEWS, write release notes for 0.4 --- NEWS | 18 ++++++++++ NEWS.md | 18 ++++++++++ README | 7 +--- README.md | 9 ++--- RELEASE_NOTES-0.4.md | 84 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 13 deletions(-) create mode 100644 RELEASE_NOTES-0.4.md diff --git a/NEWS b/NEWS index 9a4081e..b48b47a 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,21 @@ +v0.4 (2014-11-16) + (See RELEASE_NOTES-0.4.md for important details.) + * Add optional GPG support: GPG can be used to share the repository + between one or more users in lieu of sharing a secret key. + * New workflow: the symmetric key is now stored inside the .git + directory. Although backwards compatibility has been preserved + with repositories created by old versions of git-crypt, the + commands for setting up a repository have changed. See the + release notes file for details. + * Multiple key support: it's now possible to encrypt different parts + of a repository with different keys. + * Initial 'git-crypt status' command to report which files are + encrypted and to fix problems that are detected. + * Numerous usability, documentation, and error reporting improvements. + * Major internal code improvements that will make future development + easier. + * Initial experimental Windows support. + v0.3 (2013-04-05) * Fix 'git-crypt init' on newer versions of Git. Previously, encrypted files were not being automatically decrypted after diff --git a/NEWS.md b/NEWS.md index ce434e7..1746721 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,24 @@ News ==== +######v0.4 (2014-11-16) +(See [the release notes](RELEASE_NOTES-0.4.md) for important details.) +* Add optional GPG support: GPG can be used to share the repository + between one or more users in lieu of sharing a secret key. +* New workflow: the symmetric key is now stored inside the .git + directory. Although backwards compatibility has been preserved + with repositories created by old versions of git-crypt, the + commands for setting up a repository have changed. See the + release notes file for details. +* Multiple key support: it's now possible to encrypt different parts + of a repository with different keys. +* Initial `git-crypt status` command to report which files are + encrypted and to fix problems that are detected. +* Numerous usability, documentation, and error reporting improvements. +* Major internal code improvements that will make future development + easier. +* Initial experimental Windows support. + ######v0.3 (2013-04-05) * Fix `git-crypt init` on newer versions of Git. Previously, encrypted files were not being automatically decrypted after running diff --git a/README b/README index 297f855..41936da 100644 --- a/README +++ b/README @@ -66,18 +66,13 @@ encryption and decryption happen transparently. CURRENT STATUS -The latest version of git-crypt is 0.3, released on 2013-04-05. +The latest version of git-crypt is 0.4, released on 2014-11-16. git-crypt aims to be bug-free and reliable, meaning it shouldn't crash, malfunction, or expose your confidential data. However, it has not yet reached maturity, meaning it is not as documented, featureful, or easy-to-use as it should be. Additionally, there may be backwards-incompatible changes introduced before version 1.0. -Development on git-crypt is currently focused on improving the user -experience, especially around setting up repositories. There are also -plans to add additional key management schemes, such as passphrase-derived -keys and keys encrypted with PGP. - SECURITY diff --git a/README.md b/README.md index 7a4db96..3a6c082 100644 --- a/README.md +++ b/README.md @@ -67,19 +67,14 @@ encryption and decryption happen transparently. Current Status -------------- -The latest version of git-crypt is [0.3](NEWS.md), released on -2013-04-05. git-crypt aims to be bug-free and reliable, meaning it +The latest version of git-crypt is [0.4](RELEASE_NOTES-0.4.md), released on +2014-11-16. git-crypt aims to be bug-free and reliable, meaning it shouldn't crash, malfunction, or expose your confidential data. However, it has not yet reached maturity, meaning it is not as documented, featureful, or easy-to-use as it should be. Additionally, there may be backwards-incompatible changes introduced before version 1.0. -Development on git-crypt is currently focused on improving the user -experience, especially around setting up repositories. There are also -plans to add additional key management schemes, such as -passphrase-derived keys and keys encrypted with PGP. - Security -------- diff --git a/RELEASE_NOTES-0.4.md b/RELEASE_NOTES-0.4.md new file mode 100644 index 0000000..41e2634 --- /dev/null +++ b/RELEASE_NOTES-0.4.md @@ -0,0 +1,84 @@ +Changes to be aware of for git-crypt 0.4 +======================================== + +(For a complete list of changes, see the [NEWS](NEWS.md) file.) + + +### New workflow + +The commands for setting up a repository have changed in git-crypt 0.4. +The previous commands continue to work, but will be removed in a future +release of git-crypt. Please get in the habit of using the new syntax: + +`git-crypt init` no longer takes an argument, and is now used only when +initializing a repository for the very first time. It generates a key +and stores it in the `.git` directory. There is no longer a separate +`keygen` step, and you no longer need to keep a copy of the key outside +the repository. + +`git-crypt init` is no longer used to decrypt a cloned repository. Instead, +run `git-crypt unlock /path/to/keyfile`, where `keyfile` is obtained by +running `git-crypt export-key /path/to/keyfile` from an already-decrypted +repository. + + +### GPG mode + +git-crypt now supports GPG. A repository can be shared with one or more +GPG users in lieu of sharing a secret symmetric key. Symmetric key support +isn't going away, but the workflow of GPG mode is extremely easy and all users +are encouraged to consider it for their repositories. + +See the [README](README.md) for details on using GPG. + + +### Status command + +A new command, `git-crypt status`, lists encrypted files, which is +useful for making sure your `.gitattributes` pattern is protecting the +right files. + + +### Multiple key support + +git-crypt now lets you encrypt different sets of files with different +keys, which is useful if you want to grant different collaborators access +to different sets of files. + +See [doc/multiple_keys.md](doc/multiple_keys.md) for details. + + +### Compatibility with old repositories + +Repositories created with older versions of git-crypt continue to work +without any changes needed, and backwards compatibility with these +repositories will be maintained indefinitely. + +However, you will not be able to take advantage of git-crypt's new +features, such as GPG support, unless you migrate your repository. + +To migrate your repository, first ensure the working tree is clean. +Then migrate your current key file and use the migrated key to unlock +your repository as follows: + + git-crypt migrate-key /path/to/old_key /path/to/migrated_key + git-crypt unlock /path/to/migrated_key + +Once you've confirmed that your repository is functional, you can delete +both the old and migrated key files (though keeping a backup of your key +is always a good idea). + + +### Known issues + +It is not yet possible to revoke access from a GPG user. This will +require substantial development work and will be a major focus of future +git-crypt development. + +The output of `git-crypt status` is currently very bare-bones and will +be substantially improved in a future release. Do not rely on its output +being stable. A future release of git-crypt will provide an option for stable +machine-readable output. + +On Windows, git-crypt does not create key files with restrictive +permissions. Take care when using git-crypt on a multi-user Windows system. From 1b1715b5ec757eca91e22bbcbe2a9ae7a4c16ee9 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Wed, 26 Nov 2014 09:38:19 -0800 Subject: [PATCH 86/97] README: use https URLs for mailing lists --- README | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README b/README index 41936da..0bbf99a 100644 --- a/README +++ b/README @@ -145,5 +145,5 @@ MAILING LISTS To stay abreast of, and provide input to, git-crypt development, consider subscribing to one or both of our mailing lists: -Announcements: http://lists.cloudmutt.com/mailman/listinfo/git-crypt-announce -Discussion: http://lists.cloudmutt.com/mailman/listinfo/git-crypt-discuss +Announcements: https://lists.cloudmutt.com/mailman/listinfo/git-crypt-announce +Discussion: https://lists.cloudmutt.com/mailman/listinfo/git-crypt-discuss diff --git a/README.md b/README.md index 3a6c082..965d1c4 100644 --- a/README.md +++ b/README.md @@ -147,5 +147,5 @@ Mailing Lists To stay abreast of, and provide input to, git-crypt development, consider subscribing to one or both of our mailing lists: -* [Announcements](http://lists.cloudmutt.com/mailman/listinfo/git-crypt-announce) -* [Discussion](http://lists.cloudmutt.com/mailman/listinfo/git-crypt-discuss) +* [Announcements](https://lists.cloudmutt.com/mailman/listinfo/git-crypt-announce) +* [Discussion](https://lists.cloudmutt.com/mailman/listinfo/git-crypt-discuss) From 9cb1ad3c330eae5b28fee1458e43d609f666f6f5 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Fri, 2 Jan 2015 10:30:47 -0800 Subject: [PATCH 87/97] Add some helpers for getting paths to state directories --- commands.cpp | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/commands.cpp b/commands.cpp index 6d993ce..658b63f 100644 --- a/commands.cpp +++ b/commands.cpp @@ -146,7 +146,7 @@ static void validate_key_name_or_throw (const char* key_name) } } -static std::string get_internal_keys_path () +static std::string get_internal_state_path () { // git rev-parse --git-dir std::vector command; @@ -162,11 +162,21 @@ static std::string get_internal_keys_path () std::string path; std::getline(output, path); - path += "/git-crypt/keys"; + path += "/git-crypt"; return path; } +static std::string get_internal_keys_path (const std::string& internal_state_path) +{ + return internal_state_path + "/keys"; +} + +static std::string get_internal_keys_path () +{ + return get_internal_keys_path(get_internal_state_path()); +} + static std::string get_internal_key_path (const char* key_name) { std::string path(get_internal_keys_path()); @@ -176,7 +186,7 @@ static std::string get_internal_key_path (const char* key_name) return path; } -static std::string get_repo_keys_path () +static std::string get_repo_state_path () { // git rev-parse --show-toplevel std::vector command; @@ -198,10 +208,20 @@ static std::string get_repo_keys_path () throw Error("Could not determine Git working tree - is this a non-bare repo?"); } - path += "/.git-crypt/keys"; + path += "/.git-crypt"; return path; } +static std::string get_repo_keys_path (const std::string& repo_state_path) +{ + return repo_state_path + "/keys"; +} + +static std::string get_repo_keys_path () +{ + return get_repo_keys_path(get_repo_state_path()); +} + static std::string get_path_to_top () { // git rev-parse --show-cdup @@ -1015,10 +1035,10 @@ int add_gpg_user (int argc, const char** argv) return 1; } - std::string keys_path(get_repo_keys_path()); + const std::string state_path(get_repo_state_path()); std::vector new_files; - encrypt_repo_key(key_name, *key, collab_keys, keys_path, &new_files); + encrypt_repo_key(key_name, *key, collab_keys, get_repo_keys_path(state_path), &new_files); // add/commit the new files if (!new_files.empty()) { From b7c608da25fca09a95783abf313f140c3d11cd27 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Fri, 2 Jan 2015 10:35:57 -0800 Subject: [PATCH 88/97] Add .gitattributes file to .git-crypt dir to prevent encryption Previously, if you had a .gitattributes file in the root of your repository that matched `*`, the files under .git-crypt would also be encrypted, rendering the repository un-decryptable, unless you explicitly excluded the .git-crypt directory, which was easy to overlook. Now, `git-crypt add-gpg-user` automatically adds a .gitattributes file to the .git-crypt directory to prevent its encryption. IMPORTANT: If you are currently using GPG mode to encrypt an entire repository, it is strongly advised that you upgrade git-crypt and then do the following to ensure that the files inside .git-crypt are stored properly: 1. Remove existing key files: `rm .git-crypt/keys/*/0/*` 2. Re-add GPG user(s): `git-crypt add-gpg-user GPG_USER_ID ...` --- commands.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/commands.cpp b/commands.cpp index 658b63f..d5e141a 100644 --- a/commands.cpp +++ b/commands.cpp @@ -1040,6 +1040,19 @@ int add_gpg_user (int argc, const char** argv) encrypt_repo_key(key_name, *key, collab_keys, get_repo_keys_path(state_path), &new_files); + // Add a .gitatributes file to the repo state directory to prevent files in it from being encrypted. + const std::string state_gitattributes_path(state_path + "/.gitattributes"); + if (access(state_gitattributes_path.c_str(), F_OK) != 0) { + std::ofstream state_gitattributes_file(state_gitattributes_path.c_str()); + state_gitattributes_file << "* !filter !diff\n"; + state_gitattributes_file.close(); + if (!state_gitattributes_file) { + std::clog << "Error: unable to write " << state_gitattributes_path << std::endl; + return 1; + } + new_files.push_back(state_gitattributes_path); + } + // add/commit the new files if (!new_files.empty()) { // git add NEW_FILE ... From 280bd43ac77314945db06f54417b2686f3a0a15c Mon Sep 17 00:00:00 2001 From: "Wael M. Nasreddine" Date: Thu, 27 Nov 2014 10:23:15 -0800 Subject: [PATCH 89/97] Makefile: The install target should depend git-crypt. Signed-off-by: Andrew Ayer --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index bef297d..463b502 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ util.o: util.cpp util-unix.cpp util-win32.cpp clean: rm -f *.o git-crypt -install: +install: git-crypt install -m 755 git-crypt $(DESTDIR)$(PREFIX)/bin/ .PHONY: all clean install From 12881f65fd046f06f7535e2e44d828311232e374 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Wed, 7 Jan 2015 20:22:00 -0800 Subject: [PATCH 90/97] Add 'git-crypt version' command --- git-crypt.cpp | 17 +++++++++++++++++ git-crypt.hpp | 2 ++ 2 files changed, 19 insertions(+) diff --git a/git-crypt.cpp b/git-crypt.cpp index 816bf2e..9505834 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -78,6 +78,11 @@ static void print_usage (std::ostream& out) out << "See 'git-crypt help COMMAND' for more information on a specific command." << std::endl; } +static void print_version (std::ostream& out) +{ + out << "git-crypt " << VERSION << std::endl; +} + static bool help_for_command (const char* command, std::ostream& out) { if (std::strcmp(command, "init") == 0) { @@ -121,6 +126,12 @@ static int help (int argc, const char** argv) return 0; } +static int version (int argc, const char** argv) +{ + print_version(std::cout); + return 0; +} + int main (int argc, const char** argv) try { @@ -141,6 +152,9 @@ try { if (std::strcmp(argv[arg_index], "--help") == 0) { print_usage(std::clog); return 0; + } else if (std::strcmp(argv[arg_index], "--version") == 0) { + print_version(std::clog); + return 0; } else if (std::strcmp(argv[arg_index], "--") == 0) { ++arg_index; break; @@ -171,6 +185,9 @@ try { if (std::strcmp(command, "help") == 0) { return help(argc, argv); } + if (std::strcmp(command, "version") == 0) { + return version(argc, argv); + } if (std::strcmp(command, "init") == 0) { return init(argc, argv); } diff --git a/git-crypt.hpp b/git-crypt.hpp index 94b1ded..6df62d8 100644 --- a/git-crypt.hpp +++ b/git-crypt.hpp @@ -31,6 +31,8 @@ #ifndef GIT_CRYPT_GIT_CRYPT_HPP #define GIT_CRYPT_GIT_CRYPT_HPP +#define VERSION "0.4.0" + extern const char* argv0; // initialized in main() to argv[0] #endif From 849401d7336bccfc448b9529c06428ce7643f81d Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Wed, 7 Jan 2015 20:23:28 -0800 Subject: [PATCH 91/97] Update for git-crypt 0.4.1 --- NEWS | 5 +++++ NEWS.md | 5 +++++ README | 2 +- README.md | 4 ++-- RELEASE_NOTES-0.4.1.md | 21 +++++++++++++++++++++ git-crypt.hpp | 2 +- 6 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 RELEASE_NOTES-0.4.1.md diff --git a/NEWS b/NEWS index b48b47a..c399679 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,8 @@ +v0.4.1 (2015-01-08) + * Important usability fix to ensure that the .git-crypt directory + can't be encrypted by accident (see RELEASE_NOTES-0.4.1.md for + more information). + v0.4 (2014-11-16) (See RELEASE_NOTES-0.4.md for important details.) * Add optional GPG support: GPG can be used to share the repository diff --git a/NEWS.md b/NEWS.md index 1746721..ab1de35 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,11 @@ News ==== +######v0.4.1 (2015-01-08) +* Important usability fix to ensure that the .git-crypt directory + can't be encrypted by accident (see + [the release notes](RELEASE_NOTES-0.4.1.md) for more information). + ######v0.4 (2014-11-16) (See [the release notes](RELEASE_NOTES-0.4.md) for important details.) * Add optional GPG support: GPG can be used to share the repository diff --git a/README b/README index 0bbf99a..0f10d7c 100644 --- a/README +++ b/README @@ -66,7 +66,7 @@ encryption and decryption happen transparently. CURRENT STATUS -The latest version of git-crypt is 0.4, released on 2014-11-16. +The latest version of git-crypt is 0.4.1, released on 2015-01-08. git-crypt aims to be bug-free and reliable, meaning it shouldn't crash, malfunction, or expose your confidential data. However, it has not yet reached maturity, meaning it is not as documented, diff --git a/README.md b/README.md index 965d1c4..ac59e9d 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,8 @@ encryption and decryption happen transparently. Current Status -------------- -The latest version of git-crypt is [0.4](RELEASE_NOTES-0.4.md), released on -2014-11-16. git-crypt aims to be bug-free and reliable, meaning it +The latest version of git-crypt is [0.4.1](RELEASE_NOTES-0.4.1.md), released on +2015-01-08. git-crypt aims to be bug-free and reliable, meaning it shouldn't crash, malfunction, or expose your confidential data. However, it has not yet reached maturity, meaning it is not as documented, featureful, or easy-to-use as it should be. Additionally, diff --git a/RELEASE_NOTES-0.4.1.md b/RELEASE_NOTES-0.4.1.md new file mode 100644 index 0000000..ec64dd6 --- /dev/null +++ b/RELEASE_NOTES-0.4.1.md @@ -0,0 +1,21 @@ +git-crypt 0.4.1 is a bugfix-only release that contains an important +usability fix for users who use GPG mode to encrypt an entire repository. + +Previously, if you used a '*' pattern in the top-level .gitattributes +file, and you did not explicitly add a pattern to exclude the .git-crypt +directory, the files contained therein would be encrypted, rendering +the repository impossible to unlock with GPG. + +git-crypt now adds a .gitattributes file to the .git-crypt directory +to prevent its contents from being encrypted, regardless of patterns in +the top-level .gitattributes. + +If you are using git-crypt in GPG mode to encrypt an entire repository, +and you do not already have a .gitattributes pattern to exclude the +.git-crypt directory, you are strongly advised to upgrade. After +upgrading, you should do the following in each of your repositories to +ensure that the information inside .git-crypt is properly stored: + +1. Remove existing key files: `rm .git-crypt/keys/*/0/*` + +2. Re-add GPG user(s): `git-crypt add-gpg-user GPG_USER_ID ...` diff --git a/git-crypt.hpp b/git-crypt.hpp index 6df62d8..c298559 100644 --- a/git-crypt.hpp +++ b/git-crypt.hpp @@ -31,7 +31,7 @@ #ifndef GIT_CRYPT_GIT_CRYPT_HPP #define GIT_CRYPT_GIT_CRYPT_HPP -#define VERSION "0.4.0" +#define VERSION "0.4.1" extern const char* argv0; // initialized in main() to argv[0] From 02c52ab21a716139762389fd7babf90f6baf987c Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Tue, 27 Jan 2015 21:04:22 -0800 Subject: [PATCH 92/97] Disable message about unimplemented ls-gpg-users command --- commands.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands.cpp b/commands.cpp index d5e141a..fb61b16 100644 --- a/commands.cpp +++ b/commands.cpp @@ -850,7 +850,7 @@ int unlock (int argc, const char** argv) 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; + // TODO std::clog << "To see a list of GPG keys authorized to unlock this repository, run 'git-crypt ls-gpg-users'." << std::endl; return 1; } } From 216aa27009a185f6427af24321f9b718b6fdfc85 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Tue, 27 Jan 2015 21:04:58 -0800 Subject: [PATCH 93/97] Add helper function to get attribute name for a given key --- commands.cpp | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/commands.cpp b/commands.cpp index fb61b16..e2ce435 100644 --- a/commands.cpp +++ b/commands.cpp @@ -49,6 +49,17 @@ #include #include +static std::string attribute_name (const char* key_name) +{ + if (key_name) { + // named key + return std::string("git-crypt-") + key_name; + } else { + // default key + return "git-crypt"; + } +} + static void git_config (const std::string& name, const std::string& value) { std::vector command; @@ -99,15 +110,8 @@ static void configure_git_filters (const char* key_name) static void unconfigure_git_filters (const char* key_name) { // unconfigure the git-crypt filters - if (key_name) { - // named key - git_unconfig(std::string("filter.git-crypt-") + key_name); - git_unconfig(std::string("diff.git-crypt-") + key_name); - } else { - // default key - git_unconfig("filter.git-crypt"); - git_unconfig("diff.git-crypt"); - } + git_unconfig("filter." + attribute_name(key_name)); + git_unconfig("diff." + attribute_name(key_name)); } static bool git_checkout_head (const std::string& top_dir) From 2d2053296f7f17d82a0cc0c66c6151fdf295901b Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Tue, 27 Jan 2015 21:06:29 -0800 Subject: [PATCH 94/97] Fix placement of quotes in an error message --- commands.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commands.cpp b/commands.cpp index e2ce435..8d68959 100644 --- a/commands.cpp +++ b/commands.cpp @@ -803,7 +803,7 @@ int unlock (int argc, const char** argv) // 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 << "Error: Working directory not clean." << std::endl; - std::clog << "Please commit your changes or 'git stash' them before running 'git-crypt' unlock." << std::endl; + std::clog << "Please commit your changes or 'git stash' them before running 'git-crypt unlock'." << std::endl; return 1; } @@ -938,7 +938,7 @@ int lock (int argc, const char** argv) // 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 << "Error: Working directory not clean." << std::endl; - std::clog << "Please commit your changes or 'git stash' them before running 'git-crypt' lock." << std::endl; + std::clog << "Please commit your changes or 'git stash' them before running 'git-crypt lock'." << std::endl; return 1; } From d5670c95523ec3c6abb20e35bfceb635413dfb5e Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Tue, 27 Jan 2015 21:15:07 -0800 Subject: [PATCH 95/97] Force Git to check out files by touching their mtimes Starting with Git 2.2.2, `git checkout -f HEAD` no longer checks out files if their mtimes haven't changed. This causes files to remain encrypted in the work tree after running `git-crypt unlock`, and to remain decrypted after running `git-crypt lock`'. To fix this, git-crypt now figures out what files are encrypted (by checking `git check-attr` on every file output by `git ls-files`), touches those files, and then runs `git checkout` on them. --- commands.cpp | 128 ++++++++++++++++++++++++++------------------------- 1 file changed, 65 insertions(+), 63 deletions(-) diff --git a/commands.cpp b/commands.cpp index 8d68959..ef40b21 100644 --- a/commands.cpp +++ b/commands.cpp @@ -114,20 +114,16 @@ static void unconfigure_git_filters (const char* key_name) git_unconfig("diff." + attribute_name(key_name)); } -static bool git_checkout_head (const std::string& top_dir) +static bool git_checkout (const std::vector& paths) { std::vector command; command.push_back("git"); command.push_back("checkout"); - command.push_back("-f"); - command.push_back("HEAD"); command.push_back("--"); - if (top_dir.empty()) { - command.push_back("."); - } else { - command.push_back(top_dir); + for (std::vector::const_iterator path(paths.begin()); path != paths.end(); ++path) { + command.push_back(*path); } if (!successful_exit(exec_command(command))) { @@ -260,18 +256,6 @@ static void get_git_status (std::ostream& output) } } -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)); -} - // returns filter and diff attributes as a pair static std::pair get_file_attributes (const std::string& filename) { @@ -372,6 +356,35 @@ static bool check_if_file_is_encrypted (const std::string& filename) return check_if_blob_is_encrypted(object_id); } +static void get_encrypted_files (std::vector& files, const char* key_name) +{ + // git ls-files -cz -- path_to_top + std::vector command; + command.push_back("git"); + command.push_back("ls-files"); + command.push_back("-cz"); + command.push_back("--"); + const std::string path_to_top(get_path_to_top()); + if (!path_to_top.empty()) { + command.push_back(path_to_top); + } + + std::stringstream output; + if (!successful_exit(exec_command(command, output))) { + throw Error("'git ls-files' failed - is this a Git repository?"); + } + + while (output.peek() != -1) { + std::string filename; + std::getline(output, filename, '\0'); + + // TODO: get file attributes en masse for efficiency... unfortunately this requires machine-parseable output from git check-attr to be workable, and this is only supported in Git 1.8.5 and above (released 27 Nov 2013) + if (get_file_attributes(filename).first == attribute_name(key_name)) { + files.push_back(filename); + } + } +} + 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) { @@ -785,23 +798,16 @@ void help_unlock (std::ostream& out) } 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 - // want the user to lose any changes. 'git checkout -f HEAD' doesn't touch - // untracked files so it's safe to ignore those. + // 1. Make sure working directory is clean (ignoring untracked files) + // We do this because we check out files later, and we don't want the + // user to lose any changes. (TODO: only care if encrypted files are + // modified, since we only check out encrypted files) // Running 'git status' also serves as a check that the Git repo is accessible. std::stringstream status_output; 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. + if (status_output.peek() != -1) { std::clog << "Error: Working directory not clean." << std::endl; std::clog << "Please commit your changes or 'git stash' them before running 'git-crypt unlock'." << std::endl; return 1; @@ -861,6 +867,7 @@ int unlock (int argc, const char** argv) // 4. Install the key(s) and configure the git filters + std::vector encrypted_files; for (std::vector::iterator key_file(key_files.begin()); key_file != key_files.end(); ++key_file) { std::string internal_key_path(get_internal_key_path(key_file->get_key_name())); // TODO: croak if internal_key_path already exists??? @@ -871,18 +878,18 @@ int unlock (int argc, const char** argv) } configure_git_filters(key_file->get_key_name()); + get_encrypted_files(encrypted_files, 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. - // If HEAD doesn't exist (perhaps because this repo doesn't have any files yet) - // just skip the checkout. - if (head_exists) { - if (!git_checkout_head(path_to_top)) { - 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; - } + // 5. Check out the files that are currently encrypted. + // Git won't check out a file if its mtime hasn't changed, so touch every file first. + for (std::vector::const_iterator file(encrypted_files.begin()); file != encrypted_files.end(); ++file) { + touch_file(*file); + } + if (!git_checkout(encrypted_files)) { + 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; @@ -920,23 +927,16 @@ int lock (int argc, const char** argv) 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 - // untracked files so it's safe to ignore those. + // 1. Make sure working directory is clean (ignoring untracked files) + // We do this because we check out files later, and we don't want the + // user to lose any changes. (TODO: only care if encrypted files are + // modified, since we only check out encrypted files) // Running 'git status' also serves as a check that the Git repo is accessible. std::stringstream status_output; 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. + if (status_output.peek() != -1) { std::clog << "Error: Working directory not clean." << std::endl; std::clog << "Please commit your changes or 'git stash' them before running 'git-crypt lock'." << std::endl; return 1; @@ -948,6 +948,7 @@ int lock (int argc, const char** argv) std::string path_to_top(get_path_to_top()); // 3. unconfigure the git filters and remove decrypted keys + std::vector encrypted_files; if (all_keys) { // unconfigure for all keys std::vector dirents = get_directory_contents(get_internal_keys_path().c_str()); @@ -956,6 +957,7 @@ int lock (int argc, const char** argv) const char* this_key_name = (*dirent == "default" ? 0 : dirent->c_str()); remove_file(get_internal_key_path(this_key_name)); unconfigure_git_filters(this_key_name); + get_encrypted_files(encrypted_files, this_key_name); } } else { // just handle the given key @@ -971,18 +973,18 @@ int lock (int argc, const char** argv) remove_file(internal_key_path); unconfigure_git_filters(key_name); + get_encrypted_files(encrypted_files, key_name); } - // 4. Do a force checkout so any files that were previously checked out decrypted - // will now be checked out encrypted. - // If HEAD doesn't exist (perhaps because this repo doesn't have any files yet) - // just skip the checkout. - if (head_exists) { - if (!git_checkout_head(path_to_top)) { - std::clog << "Error: 'git checkout' failed" << std::endl; - std::clog << "git-crypt has been locked but up but existing decrypted files have not been encrypted" << std::endl; - return 1; - } + // 4. Check out the files that are currently decrypted but should be encrypted. + // Git won't check out a file if its mtime hasn't changed, so touch every file first. + for (std::vector::const_iterator file(encrypted_files.begin()); file != encrypted_files.end(); ++file) { + touch_file(*file); + } + if (!git_checkout(encrypted_files)) { + std::clog << "Error: 'git checkout' failed" << std::endl; + std::clog << "git-crypt has been locked but up but existing decrypted files have not been encrypted" << std::endl; + return 1; } return 0; From 5fd36a7ac50b5e9d483c9de9b38bbe98bd3f0c8b Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Tue, 27 Jan 2015 21:21:17 -0800 Subject: [PATCH 96/97] Increase minimum supported Git version to 1.7.2 Previously, git-crypt claimed to support Git as old as 1.6.0 (albeit with degraded operation). However, this has not been true for some time, since Git 1.6.0 does not support the --porcelain option to `git status`. Since Git 1.7.2 was the first version of Git to support filters with `git blame`, was released almost five years ago (in July 2010), and is even in Debian Squeeze, it seems like a good minimum version to require. --- INSTALL | 4 +--- INSTALL.md | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/INSTALL b/INSTALL index a3e48c8..9c8e8e0 100644 --- a/INSTALL +++ b/INSTALL @@ -2,10 +2,8 @@ DEPENDENCIES To use git-crypt, you need: - * Git 1.6.0 or newer + * Git 1.7.2 or newer * OpenSSL - * For decrypted git diff output, Git 1.6.1 or newer - * For decrypted git blame output, Git 1.7.2 or newer To build git-crypt, you need a C++ compiler and OpenSSL development headers. diff --git a/INSTALL.md b/INSTALL.md index e348047..b4ee879 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -3,10 +3,8 @@ Dependencies To use git-crypt, you need: -* Git 1.6.0 or newer +* Git 1.7.2 or newer * OpenSSL -* For decrypted git diff output, Git 1.6.1 or newer -* For decrypted git blame output, Git 1.7.2 or newer To build git-crypt, you need a C++ compiler and OpenSSL development headers. From 1ca8f89602cd8530d8eac90311066a0ac7ca9720 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Sat, 31 Jan 2015 19:17:43 -0800 Subject: [PATCH 97/97] Prepare for 0.4.2 release --- NEWS | 5 +++++ NEWS.md | 5 +++++ README | 2 +- README.md | 4 ++-- git-crypt.hpp | 2 +- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/NEWS b/NEWS index c399679..f51ea1f 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,8 @@ +v0.4.2 (2015-01-31) + * Fix unlock and lock under Git 2.2.2 and higher. + * Drop support for versions of Git older than 1.7.2. + * Minor improvements to some help/error messages. + v0.4.1 (2015-01-08) * Important usability fix to ensure that the .git-crypt directory can't be encrypted by accident (see RELEASE_NOTES-0.4.1.md for diff --git a/NEWS.md b/NEWS.md index ab1de35..5da8f6b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,11 @@ News ==== +######v0.4.2 (2015-01-31) +* Fix unlock and lock under Git 2.2.2 and higher. +* Drop support for versions of Git older than 1.7.2. +* Minor improvements to some help/error messages. + ######v0.4.1 (2015-01-08) * Important usability fix to ensure that the .git-crypt directory can't be encrypted by accident (see diff --git a/README b/README index 0f10d7c..2d90b29 100644 --- a/README +++ b/README @@ -66,7 +66,7 @@ encryption and decryption happen transparently. CURRENT STATUS -The latest version of git-crypt is 0.4.1, released on 2015-01-08. +The latest version of git-crypt is 0.4.2, released on 2015-01-31. git-crypt aims to be bug-free and reliable, meaning it shouldn't crash, malfunction, or expose your confidential data. However, it has not yet reached maturity, meaning it is not as documented, diff --git a/README.md b/README.md index ac59e9d..c5759c4 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,8 @@ encryption and decryption happen transparently. Current Status -------------- -The latest version of git-crypt is [0.4.1](RELEASE_NOTES-0.4.1.md), released on -2015-01-08. git-crypt aims to be bug-free and reliable, meaning it +The latest version of git-crypt is [0.4.2](NEWS.md), released on +2015-01-31. git-crypt aims to be bug-free and reliable, meaning it shouldn't crash, malfunction, or expose your confidential data. However, it has not yet reached maturity, meaning it is not as documented, featureful, or easy-to-use as it should be. Additionally, diff --git a/git-crypt.hpp b/git-crypt.hpp index c298559..78b8196 100644 --- a/git-crypt.hpp +++ b/git-crypt.hpp @@ -31,7 +31,7 @@ #ifndef GIT_CRYPT_GIT_CRYPT_HPP #define GIT_CRYPT_GIT_CRYPT_HPP -#define VERSION "0.4.1" +#define VERSION "0.4.2" extern const char* argv0; // initialized in main() to argv[0]