diff --git a/Makefile b/Makefile index b7ddc8d..bef297d 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,26 @@ CXX := c++ CXXFLAGS := -Wall -pedantic -Wno-long-long -O2 -LDFLAGS := -lcrypto +LDFLAGS := PREFIX := /usr/local -OBJFILES = git-crypt.o commands.o crypto.o util.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 git-crypt: $(OBJFILES) - $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) + $(CXX) $(CXXFLAGS) -o $@ $(OBJFILES) $(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 b3180c5..636983a 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,8 +31,9 @@ #include "commands.hpp" #include "crypto.hpp" #include "util.hpp" -#include -#include +#include "key.hpp" +#include "gpg.hpp" +#include "parse_options.hpp" #include #include #include @@ -42,29 +43,404 @@ #include #include #include -#include -#include +#include +#include +#include +#include +#include + +static void git_config (const std::string& name, const std::string& value) +{ + std::vector command; + command.push_back("git"); + command.push_back("config"); + command.push_back(name); + command.push_back(value); + + 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())); + + 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("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"); + } +} + +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; + if (!validate_key_name(key_name, &reason)) { + throw Error(reason); + } +} + +static std::string get_internal_key_path (const char* key_name) +{ + // git rev-parse --git-dir + std::vector command; + command.push_back("git"); + command.push_back("rev-parse"); + command.push_back("--git-dir"); + + 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::getline(output, path); + path += "/git-crypt/keys/"; + path += key_name ? key_name : "default"; + return path; +} + +static std::string get_repo_keys_path () +{ + // git rev-parse --show-toplevel + std::vector command; + command.push_back("git"); + command.push_back("rev-parse"); + command.push_back("--show-toplevel"); + + 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::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 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)); +} + +// 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* 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); + if (!key_file_in) { + throw Error(std::string("Unable to open key file: ") + legacy_path); + } + key_file.load_legacy(key_file_in); + } 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, 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_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; + 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"); + } + 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; + } + } + 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; + { + Key_file this_version_key_file; + 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"; + 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); + } +} + +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)); + 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 -void clean (const char* keyfile) +int clean (int argc, const char** argv) { - keys_t keys; - load_keys(keyfile, &keys); + 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 clean [--key-name=NAME] [--key-file=PATH]" << std::endl; + return 2; + } + Key_file key_file; + load_key(key_file, key_name, key_path, 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 + 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]; + 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(); + const size_t bytes_read = std::cin.gcount(); hmac.add(reinterpret_cast(buffer), bytes_read); file_size += bytes_read; @@ -73,19 +449,18 @@ void clean (const char* keyfile) 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); } } // 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,205 +482,749 @@ 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) { + 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; + 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(); + const 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; +} + +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 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; } // Decrypt contents of stdin and write to stdout -void smudge (const char* keyfile) +int smudge (int argc, const char** argv) { - keys_t keys; - load_keys(keyfile, &keys); + 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 [--key-name=NAME] [--key-file=PATH]" << std::endl; + return 2; + } + Key_file key_file; + 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 - 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.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; + 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; } - process_stream(std::cin, std::cout, &keys.enc, reinterpret_cast(header + 10)); + return decrypt_file_to_stdout(key_file, header, std::cin); } -void diff (const char* keyfile, const char* filename) +int diff (int argc, const char** argv) { - keys_t keys; - load_keys(keyfile, &keys); + 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 [--key-name=NAME] [--key-file=PATH] FILENAME" << std::endl; + return 2; + } + Key_file key_file; + load_key(key_file, key_name, key_path, 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.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()); // include the bytes which we already read + std::cout << in.rdbuf(); + return 0; } - process_stream(in, std::cout, &keys.enc, reinterpret_cast(header + 10)); + // Go ahead and decrypt it + return decrypt_file_to_stdout(key_file, header, in); } - -void init (const char* argv0, const char* keyfile) +int init (int argc, const char** argv) { - if (access(keyfile, R_OK) == -1) { - perror(keyfile); - std::exit(1); - } - - // 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; + 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)); - // 1. Make sure working directory is clean (ignoring untracked files) + 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 - argi != 0) { + std::clog << "Usage: git-crypt init [-k KEYNAME]" << std::endl; + return 2; + } + + if (key_name) { + validate_key_name_or_throw(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; + } + + // 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); + 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; + } + + // 2. Configure git for git-crypt + configure_git_filters(key_name); + + return 0; +} + +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. - 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 (status != 0) { - std::clog << "git status failed - is this a git repository?\n"; - std::exit(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. - 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); - } + std::string path_to_top(get_path_to_top()); - // 3. Add config options to git + // 3. Load the key(s) + std::vector key_files; + if (argc > 0) { + // Read from the symmetric key file(s) - std::string git_crypt_path(std::strchr(argv0, '/') ? resolve_path(argv0) : argv0); - std::string keyfile_path(resolve_path(keyfile)); + for (int argi = 0; argi < argc; ++argi) { + const char* symmetric_key_file = argv[argi]; + Key_file key_file; - // 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); - } + 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; + } - // 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); - } - - // 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); + key_files.push_back(key_file); + } + } else { + // 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 + // 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; + } } - // 4. Do a force checkout so any files that were previously checked out encrypted + // 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. // 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); - - 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 (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); + 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; } } + + return 0; } -void keygen (const char* keyfile) +int add_gpg_key (int argc, const char** argv) { - 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); + 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)); - 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); + 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; } - keyout.write(reinterpret_cast(buffer), sizeof(buffer)); + + // build a list of key fingerprints for every collaborator specified on the command line + std::vector collab_keys; + + for (int i = argi; i < argc; ++i) { + std::vector keys(gpg_lookup_key(argv[i])); + 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, key_name); + 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_name, *key, collab_keys, keys_path, &new_files); + + // add/commit the new files + if (!new_files.empty()) { + // git add NEW_FILE ... + 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; + return 1; + } + + // git commit ... + 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()); + + if (!successful_exit(exec_command(command))) { + std::clog << "Error: 'git commit' failed" << std::endl; + return 1; + } + } + } + + return 0; } + +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, const 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-gpg-keys is not yet implemented." << std::endl; + return 1; +} + +int export_key (int argc, const 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)); + + 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, key_name); + + const char* out_file_name = argv[argi]; + + if (std::strcmp(out_file_name, "-") == 0) { + key_file.store(std::cout); + } else { + if (!key_file.store_to_file(out_file_name)) { + std::clog << "Error: " << out_file_name << ": unable to write key file" << std::endl; + return 1; + } + } + + return 0; +} + +int keygen (int argc, const 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_to_file(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, const 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_to_file(new_key_file_name.c_str())) { + 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; + return 1; + } + + return 0; +} + +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, const char** argv) +{ + // 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 + + // 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 + bool fix_problems = false; // -f fix problems + bool machine_output = false; // -z machine-parseable output + + 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) { + 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" || 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); + + 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) { + // 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 + 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 ccce978..2575ca6 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,29 @@ #ifndef GIT_CRYPT_COMMANDS_HPP #define GIT_CRYPT_COMMANDS_HPP +#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, const char** argv); +int smudge (int argc, const char** argv); +int diff (int argc, const char** argv); +// Public commands: +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/crypto-openssl.cpp b/crypto-openssl.cpp new file mode 100644 index 0000000..6483e86 --- /dev/null +++ b/crypto-openssl.cpp @@ -0,0 +1,115 @@ +/* + * 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 "util.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +void init_crypto () +{ + ERR_load_crypto_strings(); +} + +struct Aes_ecb_encryptor::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 () +{ + // Note: Explicit destructor necessary because class contains an auto_ptr + // which contains an incomplete type when the auto_ptr is declared. + + explicit_memset(&impl->key, '\0', sizeof(impl->key)); +} + +void Aes_ecb_encryptor::encrypt(const unsigned char* plain, unsigned char* cipher) +{ + AES_encrypt(plain, cipher, &(impl->key)); +} + +struct Hmac_sha1_state::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 () +{ + // 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)); +} + +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 e1a8594..3ae3ecb 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. * @@ -28,102 +28,54 @@ * as that of the covered work. */ -#define _BSD_SOURCE #include "crypto.hpp" -#include -#include -#include -#include -#include -#include +#include "util.hpp" #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* nonce) +: ecb(raw_key) { - 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); - } - - // 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))); + // 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; - memset(otp, '\0', sizeof(otp)); } -void aes_ctr_state::process (const AES_KEY* key, const uint8_t* in, uint8_t* out, size_t len) +Aes_ctr_encryptor::~Aes_ctr_encryptor () +{ + explicit_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 % 16 == 0) { - // 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 = htonl(byte_counter / 16); - memcpy(ctr, nonce, 12); - memcpy(ctr + 12, &blockno, 4); - AES_encrypt(ctr, otp, key); + if (byte_counter % BLOCK_LEN == 0) { + // 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); + + // Generate a new pad + ecb.encrypt(ctr_value, pad); } // encrypt one byte - out[i] = in[i] ^ otp[byte_counter++ % 16]; + 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"); + } } } -hmac_sha1_state::hmac_sha1_state (const uint8_t* 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 uint8_t* buffer, size_t buffer_len) -{ - HMAC_Update(&ctx, buffer, buffer_len); -} - -void hmac_sha1_state::get (uint8_t* digest) -{ - unsigned int len; - HMAC_Final(&ctx, digest, &len); -} - - // 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()); } } + diff --git a/crypto.hpp b/crypto.hpp index 6bde6ab..db03241 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,87 @@ #ifndef GIT_CRYPT_CRYPTO_HPP #define GIT_CRYPT_CRYPTO_HPP -#include -#include +#include "key.hpp" #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 +void init_crypto (); + +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_ecb_encryptor { +public: + enum { + KEY_LEN = AES_KEY_LEN, + BLOCK_LEN = 16 + }; -class aes_ctr_state { - 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 +private: + struct Aes_impl; + + std::auto_ptr impl; public: - aes_ctr_state (const uint8_t* arg_nonce, size_t arg_nonce_len); - - void process (const AES_KEY* key, const uint8_t* in, uint8_t* out, size_t len); + Aes_ecb_encryptor (const unsigned char* key); + ~Aes_ecb_encryptor (); + void encrypt (const unsigned char* plain, unsigned char* cipher); }; -class hmac_sha1_state { - HMAC_CTX ctx; - - // disallow copy/assignment: - hmac_sha1_state (const hmac_sha1_state&) { } - hmac_sha1_state& operator= (const hmac_sha1_state&) { return *this; } +class Aes_ctr_encryptor { public: - hmac_sha1_state (const uint8_t* key, size_t key_len); - ~hmac_sha1_state (); + 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 + }; - void add (const uint8_t* buffer, size_t buffer_len); - void get (uint8_t*); +private: + Aes_ecb_encryptor ecb; + 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); + + // 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); }; -// 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); +typedef Aes_ctr_encryptor Aes_ctr_decryptor; +class Hmac_sha1_state { +public: + enum { + LEN = 20, + KEY_LEN = HMAC_KEY_LEN + }; + +private: + struct Hmac_impl; + + std::auto_ptr impl; + +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*); +}; + +void random_bytes (unsigned char*, size_t); #endif diff --git a/git-crypt.cpp b/git-crypt.cpp index bd58391..d0567ab 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,59 +28,172 @@ * as that of the covered work. */ +#include "git-crypt.hpp" #include "commands.hpp" #include "util.hpp" +#include "crypto.hpp" +#include "key.hpp" +#include "gpg.hpp" +#include "parse_options.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; + // |--------------------------------------------------------------------------------| 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 << " 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 << "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 << std::endl; + out << "See 'git-crypt help COMMAND' for more information on a specific command." << std::endl; + */ } int main (int argc, const char** argv) try { - // The following two lines are essential for achieving good performance: - std::ios_base::sync_with_stdio(false); - std::cin.tie(0); + argv0 = argv[0]; - std::cin.exceptions(std::ios_base::badbit); - std::cout.exceptions(std::ios_base::badbit); + /* + * General initialization + */ - if (argc < 3) { - print_usage(argv[0]); + init_std_streams(); + init_crypto(); + + /* + * Parse command line arguments + */ + 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::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; + } + } + + argc -= arg_index; + argv += arg_index; + + if (argc == 0) { + print_usage(std::clog); return 2; } - ERR_load_crypto_strings(); + /* + * Pass off to command handler + */ + const char* command = argv[0]; + --argc; + ++argv; - 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]); - return 2; + // 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-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); } - return 0; + print_usage(std::clog); + return 2; + +} 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: 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; + 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; +} 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..94b1ded --- /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_GIT_CRYPT_HPP +#define GIT_CRYPT_GIT_CRYPT_HPP + +extern const char* argv0; // initialized in main() to argv[0] + +#endif diff --git a/gpg.cpp b/gpg.cpp new file mode 100644 index 0000000..4813b35 --- /dev/null +++ b/gpg.cpp @@ -0,0 +1,176 @@ +/* + * 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::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, 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::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, 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::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(command, 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::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 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/gpg.hpp b/gpg.hpp new file mode 100644 index 0000000..cd55171 --- /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 GIT_CRYPT_GPG_HPP +#define GIT_CRYPT_GPG_HPP + +#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 new file mode 100644 index 0000000..d8fa482 --- /dev/null +++ b/key.cpp @@ -0,0 +1,336 @@ +/* + * 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 +#include +#include +#include + +Key_file::Entry::Entry () +{ + version = 0; + explicit_memset(aes_key, 0, AES_KEY_LEN); + explicit_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 + if (field_len > MAX_FIELD_LEN) { + throw Malformed(); + } + in.ignore(field_len); + if (in.gcount() != static_cast(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) { + throw Malformed(); + } + + // Then the HMAC key + in.read(reinterpret_cast(hmac_key), HMAC_KEY_LEN); + 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 +{ + // 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 (uint32_t arg_version) +{ + version = arg_version; + 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 (const Entry& entry) +{ + entries[entry.version] = entry; +} + + +void Key_file::load_legacy (std::istream& in) +{ + entries[0].load_legacy(0, 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(); + } + load_header(in); + while (in.peek() != -1) { + 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(); + } + 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(); + } + 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); + } + 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 + if (field_len > MAX_FIELD_LEN) { + throw Malformed(); + } + in.ignore(field_len); + if (in.gcount() != static_cast(field_len)) { + throw Malformed(); + } + } + } +} + +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) { + it->second.store(out); + } +} + +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) { + return false; + } + load(key_file_in); + return true; +} + +bool Key_file::store_to_file (const char* key_file_name) const +{ + create_protected_file(key_file_name); + std::ofstream key_file_out(key_file_name, std::fstream::binary); + if (!key_file_out) { + return false; + } + store(key_file_out); + key_file_out.close(); + if (!key_file_out) { + return false; + } + return true; +} + +std::string Key_file::store_to_string () const +{ + std::ostringstream ss; + store(ss); + return ss.str(); +} + +void Key_file::generate () +{ + uint32_t version(is_empty() ? 0 : latest() + 1); + entries[version].generate(version); +} + +uint32_t Key_file::latest () const +{ + if (is_empty()) { + throw std::invalid_argument("Key_file::latest"); + } + 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 new file mode 100644 index 0000000..2695581 --- /dev/null +++ b/key.hpp @@ -0,0 +1,116 @@ +/* + * 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_KEY_HPP +#define GIT_CRYPT_KEY_HPP + +#include +#include +#include +#include +#include + +enum { + HMAC_KEY_LEN = 64, + AES_KEY_LEN = 32 +}; + +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 (uint32_t version); + }; + + struct Malformed { }; // exception class + struct Incompatible { }; // exception class + + const Entry* get_latest () const; + + const Entry* get (uint32_t version) const; + void add (const Entry&); + + void load_legacy (std::istream&); + void load (std::istream&); + void store (std::ostream&) const; + + 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(); } + bool is_filled () const { return !is_empty(); } + + 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 = 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 { + MAX_FIELD_LEN = 1<<20 + }; +}; + +enum { + KEY_NAME_MAX_LEN = 128 +}; + +bool validate_key_name (const char* key_name, std::string* reason =0); + +#endif diff --git a/parse_options.cpp b/parse_options.cpp new file mode 100644 index 0000000..51b51f7 --- /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, const 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 (const 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..c0580f0 --- /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, const 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 diff --git a/util-unix.cpp b/util-unix.cpp new file mode 100644 index 0000000..2bdf364 --- /dev/null +++ b/util-unix.cpp @@ -0,0 +1,327 @@ +/* + * 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 +#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; + } + } +} + +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) { + throw System_error("pipe", "", errno); + } + pid_t child = fork(); + if (child == -1) { + int fork_errno = errno; + close(pipefd[0]); + close(pipefd[1]); + throw System_error("fork", "", fork_errno); + } + if (child == 0) { + close(pipefd[0]); + if (pipefd[1] != 1) { + dup2(pipefd[1], 1); + close(pipefd[1]); + } + execvp(command[0], command); + perror(command[0].c_str()); + _exit(-1); + } + close(pipefd[1]); + char buffer[1024]; + ssize_t bytes_read; + while ((bytes_read = read(pipefd[0], buffer, sizeof(buffer))) > 0) { + output.write(buffer, bytes_read); + } + if (bytes_read == -1) { + int read_errno = errno; + close(pipefd[0]); + throw System_error("read", "", read_errno); + } + close(pipefd[0]); + int status = 0; + if (waitpid(child, &status, 0) == -1) { + throw System_error("waitpid", "", errno); + } + return status; +} + +int exec_command_with_input (const std::vector& command, const char* p, size_t len) +{ + int pipefd[2]; + if (pipe(pipefd) == -1) { + throw System_error("pipe", "", errno); + } + pid_t child = fork(); + if (child == -1) { + int fork_errno = errno; + close(pipefd[0]); + close(pipefd[1]); + throw System_error("fork", "", fork_errno); + } + if (child == 0) { + close(pipefd[1]); + if (pipefd[0] != 0) { + dup2(pipefd[0], 0); + close(pipefd[0]); + } + execvp(command[0], command); + perror(command[0].c_str()); + _exit(-1); + } + close(pipefd[0]); + while (len > 0) { + ssize_t bytes_written = write(pipefd[1], p, len); + if (bytes_written == -1) { + int write_errno = errno; + close(pipefd[1]); + throw System_error("write", "", write_errno); + } + p += bytes_written; + len -= bytes_written; + } + close(pipefd[1]); + int status = 0; + if (waitpid(child, &status, 0) == -1) { + throw System_error("waitpid", "", errno); + } + return status; +} + +bool successful_exit (int status) +{ + return status != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0; +} + +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 () +{ +} + +void create_protected_file (const char* path) +{ + 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) +{ + 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 new file mode 100644 index 0000000..4d442db --- /dev/null +++ b/util-win32.cpp @@ -0,0 +1,386 @@ +/* + * 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 +#include +#include + +std::string System_error::message () const +{ + std::string mesg(action); + if (!target.empty()) { + mesg += ": "; + mesg += target; + } + if (error) { + 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; +} + +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 () +{ + 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); +} + +static void escape_cmdline_argument (std::string& cmdline, const std::string& arg) +{ + // For an explanation of Win32's arcane argument quoting rules, see: + // http://msdn.microsoft.com/en-us/library/17w5ykft%28v=vs.85%29.aspx + // http://msdn.microsoft.com/en-us/library/bb776391%28v=vs.85%29.aspx + // http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx + // http://blogs.msdn.com/b/oldnewthing/archive/2010/09/17/10063629.aspx + cmdline.push_back('"'); + + std::string::const_iterator p(arg.begin()); + while (p != arg.end()) { + if (*p == '"') { + cmdline.push_back('\\'); + cmdline.push_back('"'); + ++p; + } else if (*p == '\\') { + unsigned int num_backslashes = 0; + while (p != arg.end() && *p == '\\') { + ++num_backslashes; + ++p; + } + if (p == arg.end() || *p == '"') { + // Backslashes need to be escaped + num_backslashes *= 2; + } + while (num_backslashes--) { + cmdline.push_back('\\'); + } + } else { + cmdline.push_back(*p++); + } + } + + cmdline.push_back('"'); +} + +static std::string format_cmdline (const std::vector& command) +{ + std::string cmdline; + for (std::vector::const_iterator arg(command.begin()); arg != command.end(); ++arg) { + if (arg != command.begin()) { + cmdline.push_back(' '); + } + escape_cmdline_argument(cmdline, *arg); + } + return cmdline; +} + +static int wait_for_child (HANDLE child_handle) +{ + if (WaitForSingleObject(child_handle, INFINITE) == WAIT_FAILED) { + throw System_error("WaitForSingleObject", "", GetLastError()); + } + + DWORD exit_code; + if (!GetExitCodeProcess(child_handle, &exit_code)) { + throw System_error("GetExitCodeProcess", "", GetLastError()); + } + + return exit_code; +} + +static HANDLE spawn_command (const std::vector& command, HANDLE stdin_handle, HANDLE stdout_handle, HANDLE stderr_handle) +{ + PROCESS_INFORMATION proc_info; + ZeroMemory(&proc_info, sizeof(proc_info)); + + STARTUPINFO start_info; + ZeroMemory(&start_info, sizeof(start_info)); + + start_info.cb = sizeof(STARTUPINFO); + start_info.hStdInput = stdin_handle ? stdin_handle : GetStdHandle(STD_INPUT_HANDLE); + start_info.hStdOutput = stdout_handle ? stdout_handle : GetStdHandle(STD_OUTPUT_HANDLE); + start_info.hStdError = stderr_handle ? stderr_handle : GetStdHandle(STD_ERROR_HANDLE); + start_info.dwFlags |= STARTF_USESTDHANDLES; + + std::string cmdline(format_cmdline(command)); + + if (!CreateProcessA(NULL, // application name (NULL to use command line) + const_cast(cmdline.c_str()), + NULL, // process security attributes + NULL, // primary thread security attributes + TRUE, // handles are inherited + 0, // creation flags + NULL, // use parent's environment + NULL, // use parent's current directory + &start_info, + &proc_info)) { + throw System_error("CreateProcess", cmdline, GetLastError()); + } + + CloseHandle(proc_info.hThread); + + return proc_info.hProcess; +} + +int exec_command (const std::vector& command) +{ + HANDLE child_handle = spawn_command(command, NULL, NULL, NULL); + int exit_code = wait_for_child(child_handle); + CloseHandle(child_handle); + return exit_code; +} + +int exec_command (const std::vector& command, std::ostream& output) +{ + HANDLE stdout_pipe_reader = NULL; + HANDLE stdout_pipe_writer = NULL; + SECURITY_ATTRIBUTES sec_attr; + + // Set the bInheritHandle flag so pipe handles are inherited. + sec_attr.nLength = sizeof(SECURITY_ATTRIBUTES); + sec_attr.bInheritHandle = TRUE; + sec_attr.lpSecurityDescriptor = NULL; + + // Create a pipe for the child process's STDOUT. + if (!CreatePipe(&stdout_pipe_reader, &stdout_pipe_writer, &sec_attr, 0)) { + throw System_error("CreatePipe", "", GetLastError()); + } + + // Ensure the read handle to the pipe for STDOUT is not inherited. + if (!SetHandleInformation(stdout_pipe_reader, HANDLE_FLAG_INHERIT, 0)) { + throw System_error("SetHandleInformation", "", GetLastError()); + } + + HANDLE child_handle = spawn_command(command, NULL, stdout_pipe_writer, NULL); + CloseHandle(stdout_pipe_writer); + + // Read from stdout_pipe_reader. + // Note that ReadFile on a pipe may return with bytes_read==0 if the other + // end of the pipe writes zero bytes, so don't break out of the read loop + // when this happens. When the other end of the pipe closes, ReadFile + // fails with ERROR_BROKEN_PIPE. + char buffer[1024]; + DWORD bytes_read; + while (ReadFile(stdout_pipe_reader, buffer, sizeof(buffer), &bytes_read, NULL)) { + output.write(buffer, bytes_read); + } + const DWORD read_error = GetLastError(); + if (read_error != ERROR_BROKEN_PIPE) { + throw System_error("ReadFile", "", read_error); + } + + CloseHandle(stdout_pipe_reader); + + int exit_code = wait_for_child(child_handle); + CloseHandle(child_handle); + return exit_code; +} + +int exec_command_with_input (const std::vector& command, const char* p, size_t len) +{ + HANDLE stdin_pipe_reader = NULL; + HANDLE stdin_pipe_writer = NULL; + SECURITY_ATTRIBUTES sec_attr; + + // Set the bInheritHandle flag so pipe handles are inherited. + sec_attr.nLength = sizeof(SECURITY_ATTRIBUTES); + sec_attr.bInheritHandle = TRUE; + sec_attr.lpSecurityDescriptor = NULL; + + // Create a pipe for the child process's STDIN. + if (!CreatePipe(&stdin_pipe_reader, &stdin_pipe_writer, &sec_attr, 0)) { + throw System_error("CreatePipe", "", GetLastError()); + } + + // Ensure the write handle to the pipe for STDIN is not inherited. + if (!SetHandleInformation(stdin_pipe_writer, HANDLE_FLAG_INHERIT, 0)) { + throw System_error("SetHandleInformation", "", GetLastError()); + } + + HANDLE child_handle = spawn_command(command, stdin_pipe_reader, NULL, NULL); + CloseHandle(stdin_pipe_reader); + + // Write to stdin_pipe_writer. + while (len > 0) { + DWORD bytes_written; + if (!WriteFile(stdin_pipe_writer, p, len, &bytes_written, NULL)) { + throw System_error("WriteFile", "", GetLastError()); + } + p += bytes_written; + len -= bytes_written; + } + + CloseHandle(stdin_pipe_writer); + + int exit_code = wait_for_child(child_handle); + CloseHandle(child_handle); + return exit_code; +} + +bool successful_exit (int status) +{ + return status == 0; +} + +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); + _setmode(_fileno(stdout), _O_BINARY); +} + +void create_protected_file (const char* path) // TODO +{ +} + +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); +} + +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.cpp b/util.cpp index 575d616..2da0622 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,89 +28,10 @@ * as that of the covered work. */ +#include "git-crypt.hpp" #include "util.hpp" #include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -int exec_command (const char* command, std::ostream& output) -{ - int pipefd[2]; - if (pipe(pipefd) == -1) { - perror("pipe"); - std::exit(9); - } - pid_t child = fork(); - if (child == -1) { - perror("fork"); - std::exit(9); - } - if (child == 0) { - close(pipefd[0]); - if (pipefd[1] != 1) { - dup2(pipefd[1], 1); - close(pipefd[1]); - } - execl("/bin/sh", "sh", "-c", command, NULL); - 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); - } - close(pipefd[0]); - int status = 0; - waitpid(child, &status, 0); - return status; -} - -std::string resolve_path (const char* path) -{ - char* resolved_path_p = realpath(path, NULL); - std::string resolved_path(resolved_path_p); - free(resolved_path_p); - return resolved_path; -} - -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 { - 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); - if (fd == -1) { - perror("mkstemp"); - std::exit(9); - } - umask(old_umask); - file.open(path, mode); - if (!file.is_open()) { - perror("open"); - unlink(path); - std::exit(9); - } - unlink(path); - close(fd); - delete[] path; -} +#include std::string escape_shell_arg (const std::string& str) { @@ -126,3 +47,84 @@ 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); +} + +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 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 () +{ + // 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 18ea199..aa04912 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. * @@ -34,11 +34,48 @@ #include #include #include +#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); +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) { } + + 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 our_exe_path (); +int exec_command (const std::vector&); +int exec_command (const std::vector&, std::ostream& output); +int exec_command_with_input (const std::vector&, const char* p, size_t len); +bool successful_exit (int status); +void touch_file (const std::string&); 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* 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 (); +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); #endif