diff --git a/Makefile b/Makefile index f0b8e60..e3f9920 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ CXXFLAGS := -Wall -pedantic -ansi -Wno-long-long -O2 LDFLAGS := -lcrypto PREFIX := /usr/local -OBJFILES = git-crypt.o commands.o crypto.o util.o +OBJFILES = git-crypt.o commands.o crypto.o key.o util.o all: git-crypt diff --git a/commands.cpp b/commands.cpp index b3180c5..a0fc292 100644 --- a/commands.cpp +++ b/commands.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -31,6 +31,7 @@ #include "commands.hpp" #include "crypto.hpp" #include "util.hpp" +#include "key.hpp" #include #include #include @@ -42,26 +43,102 @@ #include #include #include -#include -#include +#include +#include +#include + +static void configure_git_filters () +{ + std::string git_crypt_path(our_exe_path()); + + // git config filter.git-crypt.smudge "/path/to/git-crypt smudge" + std::string command("git config filter.git-crypt.smudge "); + command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " smudge"); + + if (system(command.c_str()) != 0) { + throw Error("'git config' failed"); + } + + // git config filter.git-crypt.clean "/path/to/git-crypt clean" + command = "git config filter.git-crypt.clean "; + command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " clean"); + + if (system(command.c_str()) != 0) { + throw Error("'git config' failed"); + } + + // git config diff.git-crypt.textconv "/path/to/git-crypt diff" + command = "git config diff.git-crypt.textconv "; + command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " diff"); + + if (system(command.c_str()) != 0) { + throw Error("'git config' failed"); + } +} + +static std::string get_internal_key_path () +{ + std::stringstream output; + + if (!successful_exit(exec_command("git rev-parse --git-dir", output))) { + throw Error("'git rev-parse --git-dir' - is this a Git repository?"); + } + + std::string path; + std::getline(output, path); + path += "/git-crypt/key"; + return path; +} + +static void load_key (Key_file& key_file, const char* legacy_path =0) +{ + if (legacy_path) { + std::ifstream key_file_in(legacy_path, std::fstream::binary); + if (!key_file_in) { + throw Error(std::string("Unable to open key file: ") + legacy_path); + } + key_file.load_legacy(key_file_in); + } else { + std::ifstream key_file_in(get_internal_key_path().c_str(), std::fstream::binary); + if (!key_file_in) { + throw Error("Unable to open key file - have you unlocked/initialized this repository yet?"); + } + key_file.load(key_file_in); + } +} + // Encrypt contents of stdin and write to stdout -void clean (const char* keyfile) +int clean (int argc, char** argv) { - keys_t keys; - load_keys(keyfile, &keys); + const char* legacy_key_path = 0; + if (argc == 0) { + } else if (argc == 1) { + legacy_key_path = argv[0]; + } else { + std::clog << "Usage: git-crypt smudge" << std::endl; + return 2; + } + Key_file key_file; + load_key(key_file, legacy_key_path); + + const Key_file::Entry* key = key_file.get_latest(); + if (!key) { + std::clog << "git-crypt: error: key file is empty" << std::endl; + return 1; + } // Read the entire file - hmac_sha1_state hmac(keys.hmac, HMAC_KEY_LEN); // Calculate the file's SHA1 HMAC as we go - uint64_t file_size = 0; // Keep track of the length, make sure it doesn't get too big - std::string file_contents; // First 8MB or so of the file go here - std::fstream temp_file; // The rest of the file spills into a temporary file on disk + Hmac_sha1_state hmac(key->hmac_key, HMAC_KEY_LEN); // Calculate the file's SHA1 HMAC as we go + uint64_t file_size = 0; // Keep track of the length, make sure it doesn't get too big + std::string file_contents; // First 8MB or so of the file go here + std::fstream temp_file; // The rest of the file spills into a temporary file on disk temp_file.exceptions(std::fstream::badbit); - char buffer[1024]; + char buffer[1024]; - while (std::cin && file_size < MAX_CRYPT_BYTES) { + while (std::cin && file_size < Aes_ctr_encryptor::MAX_CRYPT_BYTES) { std::cin.read(buffer, sizeof(buffer)); size_t bytes_read = std::cin.gcount(); @@ -80,12 +157,11 @@ void clean (const char* keyfile) } // Make sure the file isn't so large we'll overflow the counter value (which would doom security) - if (file_size >= MAX_CRYPT_BYTES) { - std::clog << "File too long to encrypt securely\n"; - std::exit(1); + if (file_size >= Aes_ctr_encryptor::MAX_CRYPT_BYTES) { + std::clog << "git-crypt: error: file too long to encrypt securely" << std::endl; + return 1; } - // We use an HMAC of the file as the encryption nonce (IV) for CTR mode. // By using a hash of the file we ensure that the encryption is // deterministic so git doesn't think the file has changed when it really @@ -107,94 +183,177 @@ void clean (const char* keyfile) // looking up the nonce (which must be stored in the clear to allow for // decryption), we use an HMAC as opposed to a straight hash. - uint8_t digest[SHA1_LEN]; + // Note: Hmac_sha1_state::LEN >= Aes_ctr_encryptor::NONCE_LEN + + unsigned char digest[Hmac_sha1_state::LEN]; hmac.get(digest); // Write a header that... std::cout.write("\0GITCRYPT\0", 10); // ...identifies this as an encrypted file - std::cout.write(reinterpret_cast(digest), NONCE_LEN); // ...includes the nonce + std::cout.write(reinterpret_cast(digest), Aes_ctr_encryptor::NONCE_LEN); // ...includes the nonce // Now encrypt the file and write to stdout - aes_ctr_state state(digest, NONCE_LEN); + Aes_ctr_encryptor aes(key->aes_key, digest); // First read from the in-memory copy - const uint8_t* file_data = reinterpret_cast(file_contents.data()); - size_t file_data_len = file_contents.size(); - for (size_t i = 0; i < file_data_len; i += sizeof(buffer)) { - size_t buffer_len = std::min(sizeof(buffer), file_data_len - i); - state.process(&keys.enc, file_data + i, reinterpret_cast(buffer), buffer_len); + const unsigned char* file_data = reinterpret_cast(file_contents.data()); + size_t file_data_len = file_contents.size(); + while (file_data_len > 0) { + size_t buffer_len = std::min(sizeof(buffer), file_data_len); + aes.process(file_data, reinterpret_cast(buffer), buffer_len); std::cout.write(buffer, buffer_len); + file_data += buffer_len; + file_data_len -= buffer_len; } // Then read from the temporary file if applicable if (temp_file.is_open()) { temp_file.seekg(0); - while (temp_file) { + while (temp_file.peek() != -1) { temp_file.read(buffer, sizeof(buffer)); - size_t buffer_len = temp_file.gcount(); + size_t buffer_len = temp_file.gcount(); - state.process(&keys.enc, reinterpret_cast(buffer), reinterpret_cast(buffer), buffer_len); + aes.process(reinterpret_cast(buffer), + reinterpret_cast(buffer), + buffer_len); std::cout.write(buffer, buffer_len); } } + + return 0; } // Decrypt contents of stdin and write to stdout -void smudge (const char* keyfile) +int smudge (int argc, char** argv) { - keys_t keys; - load_keys(keyfile, &keys); + const char* legacy_key_path = 0; + if (argc == 0) { + } else if (argc == 1) { + legacy_key_path = argv[0]; + } else { + std::clog << "Usage: git-crypt smudge" << std::endl; + return 2; + } + Key_file key_file; + load_key(key_file, legacy_key_path); // Read the header to get the nonce and make sure it's actually encrypted - char header[22]; - std::cin.read(header, 22); - if (!std::cin || std::cin.gcount() != 22 || memcmp(header, "\0GITCRYPT\0", 10) != 0) { - std::clog << "File not encrypted\n"; - std::exit(1); + unsigned char header[10 + Aes_ctr_decryptor::NONCE_LEN]; + std::cin.read(reinterpret_cast(header), sizeof(header)); + if (!std::cin || std::cin.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) { + std::clog << "git-crypt: error: file not encrypted" << std::endl; + return 1; + } + const unsigned char* nonce = header + 10; + uint32_t key_version = 0; // TODO: get the version from the file header + + const Key_file::Entry* key = key_file.get(key_version); + if (!key) { + std::clog << "git-crypt: error: key version " << key_version << " not available - please unlock with the latest version of the key." << std::endl; + return 1; } - process_stream(std::cin, std::cout, &keys.enc, reinterpret_cast(header + 10)); + Aes_ctr_decryptor::process_stream(std::cin, std::cout, key->aes_key, nonce); + return 0; } -void diff (const char* keyfile, const char* filename) +int diff (int argc, char** argv) { - keys_t keys; - load_keys(keyfile, &keys); + const char* filename = 0; + const char* legacy_key_path = 0; + if (argc == 1) { + filename = argv[0]; + } else if (argc == 2) { + legacy_key_path = argv[0]; + filename = argv[1]; + } else { + std::clog << "Usage: git-crypt diff FILENAME" << std::endl; + return 2; + } + Key_file key_file; + load_key(key_file, legacy_key_path); // Open the file - std::ifstream in(filename); + std::ifstream in(filename, std::fstream::binary); if (!in) { - perror(filename); - std::exit(1); + std::clog << "git-crypt: " << filename << ": unable to open for reading" << std::endl; + return 1; } in.exceptions(std::fstream::badbit); // Read the header to get the nonce and determine if it's actually encrypted - char header[22]; - in.read(header, 22); - if (!in || in.gcount() != 22 || memcmp(header, "\0GITCRYPT\0", 10) != 0) { + unsigned char header[10 + Aes_ctr_decryptor::NONCE_LEN]; + in.read(reinterpret_cast(header), sizeof(header)); + if (!in || in.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) { // File not encrypted - just copy it out to stdout - std::cout.write(header, in.gcount()); // don't forget to include the header which we read! - char buffer[1024]; - while (in) { - in.read(buffer, sizeof(buffer)); - std::cout.write(buffer, in.gcount()); - } - return; + std::cout.write(reinterpret_cast(header), in.gcount()); // don't forget to include the header which we read! + std::cout << in.rdbuf(); + return 0; } - process_stream(in, std::cout, &keys.enc, reinterpret_cast(header + 10)); + // Go ahead and decrypt it + const unsigned char* nonce = header + 10; + uint32_t key_version = 0; // TODO: get the version from the file header + + const Key_file::Entry* key = key_file.get(key_version); + if (!key) { + std::clog << "git-crypt: error: key version " << key_version << " not available - please unlock with the latest version of the key." << std::endl; + return 1; + } + + Aes_ctr_decryptor::process_stream(in, std::cout, key->aes_key, nonce); + return 0; } - -void init (const char* argv0, const char* keyfile) +int init (int argc, char** argv) { - if (access(keyfile, R_OK) == -1) { - perror(keyfile); - std::exit(1); + if (argc == 1) { + std::clog << "Warning: 'git-crypt init' with a key file is deprecated as of git-crypt 0.4" << std::endl; + std::clog << "and will be removed in a future release. Please get in the habit of using" << std::endl; + std::clog << "'git-crypt unlock KEYFILE' instead." << std::endl; + return unlock(argc, argv); } - + if (argc != 0) { + std::clog << "Error: 'git-crypt init' takes no arguments." << std::endl; + return 2; + } + + std::string internal_key_path(get_internal_key_path()); + if (access(internal_key_path.c_str(), F_OK) == 0) { + // TODO: add a -f option to reinitialize the repo anyways (this should probably imply a refresh) + std::clog << "Error: this repository has already been initialized with git-crypt." << std::endl; + return 1; + } + + // 1. Generate a key and install it + std::clog << "Generating key..." << std::endl; + Key_file key_file; + key_file.generate(); + + mkdir_parent(internal_key_path); + if (!key_file.store(internal_key_path.c_str())) { + std::clog << "Error: " << internal_key_path << ": unable to write key file" << std::endl; + return 1; + } + + // 2. Configure git for git-crypt + configure_git_filters(); + + return 0; +} + +int unlock (int argc, char** argv) +{ + const char* symmetric_key_file = 0; + if (argc == 0) { + } else if (argc == 1) { + symmetric_key_file = argv[0]; + } else { + std::clog << "Usage: git-crypt unlock [KEYFILE]" << std::endl; + return 2; + } + // 0. Check to see if HEAD exists. See below why we do this. bool head_exists = system("git rev-parse HEAD >/dev/null 2>/dev/null") == 0; @@ -205,61 +364,67 @@ void init (const char* argv0, const char* keyfile) int status; std::stringstream status_output; status = exec_command("git status -uno --porcelain", status_output); - if (status != 0) { - std::clog << "git status failed - is this a git repository?\n"; - std::exit(1); + if (!successful_exit(status)) { + std::clog << "Error: 'git status' failed - is this a git repository?" << std::endl; + return 1; } else if (status_output.peek() != -1 && head_exists) { // We only care that the working directory is dirty if HEAD exists. // If HEAD doesn't exist, we won't be resetting to it (see below) so // it doesn't matter that the working directory is dirty. - std::clog << "Working directory not clean.\n"; - std::clog << "Please commit your changes or 'git stash' them before setting up git-crypt.\n"; - std::exit(1); + std::clog << "Error: Working directory not clean." << std::endl; + std::clog << "Please commit your changes or 'git stash' them before running 'git-crypt' unlock." << std::endl; + return 1; } // 2. Determine the path to the top of the repository. We pass this as the argument // to 'git checkout' below. (Determine the path now so in case it fails we haven't already // mucked with the git config.) std::stringstream cdup_output; - if (exec_command("git rev-parse --show-cdup", cdup_output) != 0) { - std::clog << "git rev-parse --show-cdup failed\n"; - std::exit(1); + if (!successful_exit(exec_command("git rev-parse --show-cdup", cdup_output))) { + std::clog << "Error: 'git rev-parse --show-cdup' failed" << std::endl; + return 1; } - // 3. Add config options to git - - std::string git_crypt_path(std::strchr(argv0, '/') ? resolve_path(argv0) : argv0); - std::string keyfile_path(resolve_path(keyfile)); - - // git config filter.git-crypt.smudge "git-crypt smudge /path/to/key" - std::string command("git config filter.git-crypt.smudge "); - command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " smudge " + escape_shell_arg(keyfile_path)); - - if (system(command.c_str()) != 0) { - std::clog << "git config failed\n"; - std::exit(1); + // 3. Install the key + Key_file key_file; + if (symmetric_key_file) { + // Read from the symmetric key file + try { + if (std::strcmp(symmetric_key_file, "-") == 0) { + key_file.load(std::cin); + } else { + if (!key_file.load(symmetric_key_file)) { + std::clog << "Error: " << symmetric_key_file << ": unable to read key file" << std::endl; + return 1; + } + } + } catch (Key_file::Incompatible) { + std::clog << "Error: " << symmetric_key_file << " is in an incompatible format" << std::endl; + std::clog << "Please upgrade to a newer version of git-crypt." << std::endl; + return 1; + } catch (Key_file::Malformed) { + std::clog << "Error: " << symmetric_key_file << ": not a valid git-crypt key file" << std::endl; + std::clog << "If this key was created prior to git-crypt 0.4, you need to migrate it" << std::endl; + std::clog << "by running 'git-crypt migrate-key /path/to/key/file'." << std::endl; + return 1; + } + } else { + // Decrypt GPG key from root of repo (TODO NOW) + std::clog << "Error: GPG support is not yet implemented" << std::endl; + return 1; + } + std::string internal_key_path(get_internal_key_path()); + // TODO: croak if internal_key_path already exists??? + mkdir_parent(internal_key_path); + if (!key_file.store(internal_key_path.c_str())) { + std::clog << "Error: " << internal_key_path << ": unable to write key file" << std::endl; + return 1; } - // git config filter.git-crypt.clean "git-crypt clean /path/to/key" - command = "git config filter.git-crypt.clean "; - command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " clean " + escape_shell_arg(keyfile_path)); - - if (system(command.c_str()) != 0) { - std::clog << "git config failed\n"; - std::exit(1); - } + // 4. Configure git for git-crypt + configure_git_filters(); - // git config diff.git-crypt.textconv "git-crypt diff /path/to/key" - command = "git config diff.git-crypt.textconv "; - command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " diff " + escape_shell_arg(keyfile_path)); - - if (system(command.c_str()) != 0) { - std::clog << "git config failed\n"; - std::exit(1); - } - - - // 4. Do a force checkout so any files that were previously checked out encrypted + // 5. Do a force checkout so any files that were previously checked out encrypted // will now be checked out decrypted. // If HEAD doesn't exist (perhaps because this repo doesn't have any files yet) // just skip the checkout. @@ -267,7 +432,7 @@ void init (const char* argv0, const char* keyfile) std::string path_to_top; std::getline(cdup_output, path_to_top); - command = "git checkout -f HEAD -- "; + std::string command("git checkout -f HEAD -- "); if (path_to_top.empty()) { command += "."; } else { @@ -275,37 +440,162 @@ void init (const char* argv0, const char* keyfile) } if (system(command.c_str()) != 0) { - std::clog << "git checkout failed\n"; - std::clog << "git-crypt has been set up but existing encrypted files have not been decrypted\n"; - std::exit(1); + std::clog << "Error: 'git checkout' failed" << std::endl; + std::clog << "git-crypt has been set up but existing encrypted files have not been decrypted" << std::endl; + return 1; } } + + return 0; } -void keygen (const char* keyfile) +int add_collab (int argc, char** argv) // TODO NOW { - if (access(keyfile, F_OK) == 0) { - std::clog << keyfile << ": File already exists - please remove before continuing\n"; - std::exit(1); - } - mode_t old_umask = umask(0077); // make sure key file is protected - std::ofstream keyout(keyfile); - if (!keyout) { - perror(keyfile); - std::exit(1); - } - umask(old_umask); - - std::clog << "Generating key...\n"; - std::clog.flush(); - unsigned char buffer[AES_KEY_BITS/8 + HMAC_KEY_LEN]; - if (RAND_bytes(buffer, sizeof(buffer)) != 1) { - while (unsigned long code = ERR_get_error()) { - char error_string[120]; - ERR_error_string_n(code, error_string, sizeof(error_string)); - std::clog << "Error: " << error_string << '\n'; - } - std::exit(1); - } - keyout.write(reinterpret_cast(buffer), sizeof(buffer)); + // Sketch: + // 1. Resolve the key ID to a long hex ID + // 2. Create the in-repo key directory if it doesn't exist yet. + // 3. For most recent key version KEY_VERSION (or for each key version KEY_VERSION if retroactive option specified): + // Encrypt KEY_VERSION with the GPG key and stash it in .git-crypt/keys/KEY_VERSION/LONG_HEX_ID + // if file already exists, print a notice and move on + // 4. Commit the new file(s) (if any) with a meanignful commit message, unless -n was passed + std::clog << "Error: add-collab is not yet implemented." << std::endl; + return 1; } + +int rm_collab (int argc, char** argv) // TODO +{ + std::clog << "Error: rm-collab is not yet implemented." << std::endl; + return 1; +} + +int ls_collabs (int argc, char** argv) // TODO +{ + // Sketch: + // Scan the sub-directories in .git-crypt/keys, outputting something like this: + // ==== + // Key version 0: + // 0x143DE9B3F7316900 Andrew Ayer + // 0x4E386D9C9C61702F ??? + // Key version 1: + // 0x143DE9B3F7316900 Andrew Ayer + // 0x1727274463D27F40 John Smith + // 0x4E386D9C9C61702F ??? + // ==== + // To resolve a long hex ID, use a command like this: + // gpg --options /dev/null --fixed-list-mode --batch --with-colons --list-keys 0x143DE9B3F7316900 + + std::clog << "Error: ls-collabs is not yet implemented." << std::endl; + return 1; +} + +int export_key (int argc, char** argv) +{ + // TODO: provide options to export only certain key versions + + if (argc != 1) { + std::clog << "Usage: git-crypt export-key FILENAME" << std::endl; + return 2; + } + + Key_file key_file; + load_key(key_file); + + const char* out_file_name = argv[0]; + + if (std::strcmp(out_file_name, "-") == 0) { + key_file.store(std::cout); + } else { + if (!key_file.store(out_file_name)) { + std::clog << "Error: " << out_file_name << ": unable to write key file" << std::endl; + return 1; + } + } + + return 0; +} + +int keygen (int argc, char** argv) +{ + if (argc != 1) { + std::clog << "Usage: git-crypt keygen KEYFILE" << std::endl; + return 2; + } + + const char* key_file_name = argv[0]; + + if (std::strcmp(key_file_name, "-") != 0 && access(key_file_name, F_OK) == 0) { + std::clog << key_file_name << ": File already exists" << std::endl; + return 1; + } + + std::clog << "Generating key..." << std::endl; + Key_file key_file; + key_file.generate(); + + if (std::strcmp(key_file_name, "-") == 0) { + key_file.store(std::cout); + } else { + if (!key_file.store(key_file_name)) { + std::clog << "Error: " << key_file_name << ": unable to write key file" << std::endl; + return 1; + } + } + return 0; +} + +int migrate_key (int argc, char** argv) +{ + if (argc != 1) { + std::clog << "Usage: git-crypt migrate-key KEYFILE" << std::endl; + return 2; + } + + const char* key_file_name = argv[0]; + Key_file key_file; + + try { + if (std::strcmp(key_file_name, "-") == 0) { + key_file.load_legacy(std::cin); + key_file.store(std::cout); + } else { + std::ifstream in(key_file_name, std::fstream::binary); + if (!in) { + std::clog << "Error: " << key_file_name << ": unable to open for reading" << std::endl; + return 1; + } + key_file.load_legacy(in); + in.close(); + + std::string new_key_file_name(key_file_name); + new_key_file_name += ".new"; + + if (access(new_key_file_name.c_str(), F_OK) == 0) { + std::clog << new_key_file_name << ": File already exists" << std::endl; + return 1; + } + + if (!key_file.store(new_key_file_name.c_str())) { + std::clog << "Error: " << new_key_file_name << ": unable to write key file" << std::endl; + return 1; + } + + if (rename(new_key_file_name.c_str(), key_file_name) == -1) { + std::clog << "Error: " << key_file_name << ": " << strerror(errno) << std::endl; + unlink(new_key_file_name.c_str()); + return 1; + } + } + } catch (Key_file::Malformed) { + std::clog << "Error: " << key_file_name << ": not a valid legacy git-crypt key file" << std::endl; + return 1; + } + + return 0; +} + +int refresh (int argc, char** argv) // TODO: do a force checkout, much like in unlock +{ + std::clog << "Error: refresh is not yet implemented." << std::endl; + return 1; +} + diff --git a/commands.hpp b/commands.hpp index ce68129..9772c0d 100644 --- a/commands.hpp +++ b/commands.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -31,12 +31,28 @@ #ifndef _COMMANDS_H #define _COMMANDS_H +#include -void clean (const char* keyfile); -void smudge (const char* keyfile); -void diff (const char* keyfile, const char* filename); -void init (const char* argv0, const char* keyfile); -void keygen (const char* keyfile); +struct Error { + std::string message; + + explicit Error (std::string m) : message(m) { } +}; + +// Plumbing commands: +int clean (int argc, char** argv); +int smudge (int argc, char** argv); +int diff (int argc, char** argv); +// Public commands: +int init (int argc, char** argv); +int unlock (int argc, char** argv); +int add_collab (int argc, char** argv); +int rm_collab (int argc, char** argv); +int ls_collabs (int argc, char** argv); +int export_key (int argc, char** argv); +int keygen (int argc, char** argv); +int migrate_key (int argc, char** argv); +int refresh (int argc, char** argv); #endif diff --git a/crypto.cpp b/crypto.cpp index 25c9ae0..c11d5e2 100644 --- a/crypto.cpp +++ b/crypto.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -34,80 +34,64 @@ #include #include #include -#include -#include +#include +#include +#include #include #include -void load_keys (const char* filepath, keys_t* keys) +Aes_ctr_encryptor::Aes_ctr_encryptor (const unsigned char* raw_key, const unsigned char* arg_nonce) { - std::ifstream file(filepath); - if (!file) { - perror(filepath); - std::exit(1); - } - char buffer[AES_KEY_BITS/8 + HMAC_KEY_LEN]; - file.read(buffer, sizeof(buffer)); - if (file.gcount() != sizeof(buffer)) { - std::clog << filepath << ": Premature end of key file\n"; - std::exit(1); + if (AES_set_encrypt_key(raw_key, KEY_LEN * 8, &key) != 0) { + throw Crypto_error("Aes_ctr_encryptor::Aes_ctr_encryptor", "AES_set_encrypt_key failed"); } - // First comes the AES encryption key - if (AES_set_encrypt_key(reinterpret_cast(buffer), AES_KEY_BITS, &keys->enc) != 0) { - std::clog << filepath << ": Failed to initialize AES encryption key\n"; - std::exit(1); - } - - // Then it's the HMAC key - memcpy(keys->hmac, buffer + AES_KEY_BITS/8, HMAC_KEY_LEN); -} - - -aes_ctr_state::aes_ctr_state (const uint8_t* arg_nonce, size_t arg_nonce_len) -{ - memset(nonce, '\0', sizeof(nonce)); - memcpy(nonce, arg_nonce, std::min(arg_nonce_len, sizeof(nonce))); + std::memcpy(nonce, arg_nonce, NONCE_LEN); byte_counter = 0; - memset(otp, '\0', sizeof(otp)); + std::memset(otp, '\0', sizeof(otp)); } -void aes_ctr_state::process (const AES_KEY* key, const uint8_t* in, uint8_t* out, size_t len) +void Aes_ctr_encryptor::process (const unsigned char* in, unsigned char* out, size_t len) { for (size_t i = 0; i < len; ++i) { - if (byte_counter % 16 == 0) { + if (byte_counter % BLOCK_LEN == 0) { + unsigned char ctr[BLOCK_LEN]; + + // First 12 bytes of CTR: nonce + std::memcpy(ctr, nonce, NONCE_LEN); + + // Last 4 bytes of CTR: block number (sequentially increasing with each block) (big endian) + store_be32(ctr + NONCE_LEN, byte_counter / BLOCK_LEN); + // Generate a new OTP - // CTR value: - // first 12 bytes - nonce - // last 4 bytes - block number (sequentially increasing with each block) - uint8_t ctr[16]; - uint32_t blockno = byte_counter / 16; - memcpy(ctr, nonce, 12); - store_be32(ctr + 12, blockno); - AES_encrypt(ctr, otp, key); + AES_encrypt(ctr, otp, &key); } // encrypt one byte - out[i] = in[i] ^ otp[byte_counter++ % 16]; + out[i] = in[i] ^ otp[byte_counter++ % BLOCK_LEN]; + + if (byte_counter == 0) { + throw Crypto_error("Aes_ctr_encryptor::process", "Too much data to encrypt securely"); + } } } -hmac_sha1_state::hmac_sha1_state (const uint8_t* key, size_t key_len) +Hmac_sha1_state::Hmac_sha1_state (const unsigned char* key, size_t key_len) { HMAC_Init(&ctx, key, key_len, EVP_sha1()); } -hmac_sha1_state::~hmac_sha1_state () +Hmac_sha1_state::~Hmac_sha1_state () { HMAC_cleanup(&ctx); } -void hmac_sha1_state::add (const uint8_t* buffer, size_t buffer_len) +void Hmac_sha1_state::add (const unsigned char* buffer, size_t buffer_len) { HMAC_Update(&ctx, buffer, buffer_len); } -void hmac_sha1_state::get (uint8_t* digest) +void Hmac_sha1_state::get (unsigned char* digest) { unsigned int len; HMAC_Final(&ctx, digest, &len); @@ -115,14 +99,28 @@ void hmac_sha1_state::get (uint8_t* digest) // Encrypt/decrypt an entire input stream, writing to the given output stream -void process_stream (std::istream& in, std::ostream& out, const AES_KEY* enc_key, const uint8_t* nonce) +void Aes_ctr_encryptor::process_stream (std::istream& in, std::ostream& out, const unsigned char* key, const unsigned char* nonce) { - aes_ctr_state state(nonce, 12); + Aes_ctr_encryptor aes(key, nonce); - uint8_t buffer[1024]; + unsigned char buffer[1024]; while (in) { in.read(reinterpret_cast(buffer), sizeof(buffer)); - state.process(enc_key, buffer, buffer, in.gcount()); + aes.process(buffer, buffer, in.gcount()); out.write(reinterpret_cast(buffer), in.gcount()); } } + +void random_bytes (unsigned char* buffer, size_t len) +{ + if (RAND_bytes(buffer, len) != 1) { + std::ostringstream message; + while (unsigned long code = ERR_get_error()) { + char error_string[120]; + ERR_error_string_n(code, error_string, sizeof(error_string)); + message << "OpenSSL Error: " << error_string << "; "; + } + throw Crypto_error("random_bytes", message.str()); + } +} + diff --git a/crypto.hpp b/crypto.hpp index e8166e2..69342bd 100644 --- a/crypto.hpp +++ b/crypto.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -31,53 +31,69 @@ #ifndef _CRYPTO_H #define _CRYPTO_H +#include "key.hpp" #include #include #include #include #include +#include -enum { - SHA1_LEN = 20, - NONCE_LEN = 12, - HMAC_KEY_LEN = 64, - AES_KEY_BITS = 256, - MAX_CRYPT_BYTES = (1ULL<<32)*16 // Don't encrypt more than this or the CTR value will repeat itself +struct Crypto_error { + std::string where; + std::string message; + + Crypto_error (const std::string& w, const std::string& m) : where(w), message(m) { } }; -struct keys_t { - AES_KEY enc; - uint8_t hmac[HMAC_KEY_LEN]; -}; -void load_keys (const char* filepath, keys_t* keys); +class Aes_ctr_encryptor { +public: + enum { + NONCE_LEN = 12, + KEY_LEN = AES_KEY_LEN, + BLOCK_LEN = 16, + MAX_CRYPT_BYTES = (1ULL<<32)*16 // Don't encrypt more than this or the CTR value will repeat itself + }; -class aes_ctr_state { +private: + AES_KEY key; char nonce[NONCE_LEN];// First 96 bits of counter uint32_t byte_counter; // How many bytes processed so far? - uint8_t otp[16]; // The current OTP that's in use + unsigned char otp[BLOCK_LEN]; // The current OTP that's in use public: - aes_ctr_state (const uint8_t* arg_nonce, size_t arg_nonce_len); + Aes_ctr_encryptor (const unsigned char* key, const unsigned char* nonce); - void process (const AES_KEY* key, const uint8_t* in, uint8_t* out, size_t len); + void process (const unsigned char* in, unsigned char* out, size_t len); + + // Encrypt/decrypt an entire input stream, writing to the given output stream + static void process_stream (std::istream& in, std::ostream& out, const unsigned char* key, const unsigned char* nonce); }; -class hmac_sha1_state { +typedef Aes_ctr_encryptor Aes_ctr_decryptor; + +class Hmac_sha1_state { +public: + enum { + LEN = 20, + KEY_LEN = HMAC_KEY_LEN + }; + +private: HMAC_CTX ctx; // disallow copy/assignment: - hmac_sha1_state (const hmac_sha1_state&) { } - hmac_sha1_state& operator= (const hmac_sha1_state&) { return *this; } -public: - hmac_sha1_state (const uint8_t* key, size_t key_len); - ~hmac_sha1_state (); + Hmac_sha1_state (const Hmac_sha1_state&) { } + Hmac_sha1_state& operator= (const Hmac_sha1_state&) { return *this; } - void add (const uint8_t* buffer, size_t buffer_len); - void get (uint8_t*); +public: + Hmac_sha1_state (const unsigned char* key, size_t key_len); + ~Hmac_sha1_state (); + + void add (const unsigned char* buffer, size_t buffer_len); + void get (unsigned char*); }; -// Encrypt/decrypt an entire input stream, writing to the given output stream -void process_stream (std::istream& in, std::ostream& out, const AES_KEY* enc_key, const uint8_t* nonce); - +void random_bytes (unsigned char*, size_t); #endif diff --git a/git-crypt.cpp b/git-crypt.cpp index bd58391..69d7fed 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -28,29 +28,59 @@ * as that of the covered work. */ +#include "git-crypt.hpp" #include "commands.hpp" #include "util.hpp" +#include "crypto.hpp" +#include "key.hpp" #include +#include #include +#include #include -static void print_usage (const char* argv0) +const char* argv0; + +static void print_usage (std::ostream& out) { - std::clog << "Usage: " << argv0 << " COMMAND [ARGS ...]\n"; - std::clog << "\n"; - std::clog << "Valid commands:\n"; - std::clog << " init KEYFILE - prepare the current git repo to use git-crypt with this key\n"; - std::clog << " keygen KEYFILE - generate a git-crypt key in the given file\n"; - std::clog << "\n"; - std::clog << "Plumbing commands (not to be used directly):\n"; - std::clog << " clean KEYFILE\n"; - std::clog << " smudge KEYFILE\n"; - std::clog << " diff KEYFILE FILE\n"; + out << "Usage: " << argv0 << " COMMAND [ARGS ...]" << std::endl; + out << "" << std::endl; + out << "Standard commands:" << std::endl; + out << " init - generate a key, prepare the current repo to use git-crypt" << std::endl; + out << " unlock KEYFILE - decrypt the current repo using the given symmetric key" << std::endl; + out << " export-key FILE - export the repo's symmetric key to the given file" << std::endl; + //out << " refresh - ensure all files in the repo are properly decrypted" << std::endl; + out << " help - display this help message" << std::endl; + out << " help COMMAND - display help for the given git-crypt command" << std::endl; + out << "" << std::endl; + /* + out << "GPG commands:" << std::endl; + out << " unlock - decrypt the current repo using the in-repo GPG-encrypted key" << std::endl; + out << " add-collab GPGID - add the user with the given GPG key ID as a collaborator" << std::endl; + out << " rm-collab GPGID - revoke collaborator status from the given GPG key ID" << std::endl; + out << " ls-collabs - list the GPG key IDs of collaborators" << std::endl; + out << "" << std::endl; + */ + out << "Legacy commands:" << std::endl; + out << " init KEYFILE - alias for 'unlock KEYFILE'" << std::endl; + out << " keygen KEYFILE - generate a git-crypt key in the given file" << std::endl; + out << " migrate-key FILE - migrate the given legacy key file to the latest format" << std::endl; + out << "" << std::endl; + out << "Plumbing commands (not to be used directly):" << std::endl; + out << " clean [LEGACY-KEYFILE]" << std::endl; + out << " smudge [LEGACY-KEYFILE]" << std::endl; + out << " diff [LEGACY-KEYFILE] FILE" << std::endl; } -int main (int argc, const char** argv) +int main (int argc, char** argv) try { + argv0 = argv[0]; + + /* + * General initialization + */ + // The following two lines are essential for achieving good performance: std::ios_base::sync_with_stdio(false); std::cin.tie(0); @@ -58,29 +88,115 @@ try { std::cin.exceptions(std::ios_base::badbit); std::cout.exceptions(std::ios_base::badbit); - if (argc < 3) { - print_usage(argv[0]); - return 2; - } - ERR_load_crypto_strings(); - if (strcmp(argv[1], "init") == 0 && argc == 3) { - init(argv[0], argv[2]); - } else if (strcmp(argv[1], "keygen") == 0 && argc == 3) { - keygen(argv[2]); - } else if (strcmp(argv[1], "clean") == 0 && argc == 3) { - clean(argv[2]); - } else if (strcmp(argv[1], "smudge") == 0 && argc == 3) { - smudge(argv[2]); - } else if (strcmp(argv[1], "diff") == 0 && argc == 4) { - diff(argv[2], argv[3]); - } else { - print_usage(argv[0]); + /* + * Parse command line arguments + */ + const char* profile = 0; + int arg_index = 1; + while (arg_index < argc && argv[arg_index][0] == '-') { + if (std::strcmp(argv[arg_index], "--help") == 0) { + print_usage(std::clog); + return 0; + } else if (std::strncmp(argv[arg_index], "--profile=", 10) == 0) { + profile = argv[arg_index] + 10; + ++arg_index; + } else if (std::strcmp(argv[arg_index], "-p") == 0 && arg_index + 1 < argc) { + profile = argv[arg_index + 1]; + arg_index += 2; + } else if (std::strcmp(argv[arg_index], "--") == 0) { + ++arg_index; + break; + } else { + std::clog << argv0 << ": " << argv[arg_index] << ": Unknown option" << std::endl; + print_usage(std::clog); + return 2; + } + } + + (void)(profile); // TODO: profile support + + argc -= arg_index; + argv += arg_index; + + if (argc == 0) { + print_usage(std::clog); return 2; } - return 0; + /* + * Pass off to command handler + */ + const char* command = argv[0]; + --argc; + ++argv; + + // Public commands: + if (std::strcmp(command, "help") == 0) { + print_usage(std::clog); + return 0; + } + if (std::strcmp(command, "init") == 0) { + return init(argc, argv); + } + if (std::strcmp(command, "unlock") == 0) { + return unlock(argc, argv); + } + if (std::strcmp(command, "add-collab") == 0) { + return add_collab(argc, argv); + } + if (std::strcmp(command, "rm-collab") == 0) { + return rm_collab(argc, argv); + } + if (std::strcmp(command, "ls-collabs") == 0) { + return ls_collabs(argc, argv); + } + if (std::strcmp(command, "export-key") == 0) { + return export_key(argc, argv); + } + if (std::strcmp(command, "keygen") == 0) { + return keygen(argc, argv); + } + if (std::strcmp(command, "migrate-key") == 0) { + return migrate_key(argc, argv); + } + if (std::strcmp(command, "refresh") == 0) { + return refresh(argc, argv); + } + // Plumbing commands (executed by git, not by user): + if (std::strcmp(command, "clean") == 0) { + return clean(argc, argv); + } + if (std::strcmp(command, "smudge") == 0) { + return smudge(argc, argv); + } + if (std::strcmp(command, "diff") == 0) { + return diff(argc, argv); + } + + print_usage(std::clog); + return 2; + +} catch (const Error& e) { + std::cerr << "git-crypt: Error: " << e.message << std::endl; + return 1; +} catch (const System_error& e) { + std::cerr << "git-crypt: " << e.action << ": "; + if (!e.target.empty()) { + std::cerr << e.target << ": "; + } + std::cerr << strerror(e.error) << std::endl; + return 1; +} catch (const Crypto_error& e) { + std::cerr << "git-crypt: Crypto error: " << e.where << ": " << e.message << std::endl; + return 1; +} catch (Key_file::Incompatible) { + std::cerr << "git-crypt: This repository contains a incompatible key file. Please upgrade git-crypt." << std::endl; + return 1; +} catch (Key_file::Malformed) { + std::cerr << "git-crypt: This repository contains a malformed key file. It may be corrupted." << std::endl; + return 1; } catch (const std::ios_base::failure& e) { std::cerr << "git-crypt: I/O error: " << e.what() << std::endl; return 1; diff --git a/git-crypt.hpp b/git-crypt.hpp new file mode 100644 index 0000000..00091d5 --- /dev/null +++ b/git-crypt.hpp @@ -0,0 +1,36 @@ +/* + * Copyright 2014 Andrew Ayer + * + * This file is part of git-crypt. + * + * git-crypt is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * git-crypt is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with git-crypt. If not, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify the Program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, the licensors of the Program + * grant you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#ifndef _GIT_CRYPT_H +#define _GIT_CRYPT_H + +extern const char* argv0; // initialized in main() to argv[0] + +#endif diff --git a/key.cpp b/key.cpp new file mode 100644 index 0000000..c71be60 --- /dev/null +++ b/key.cpp @@ -0,0 +1,161 @@ +/* + * Copyright 2014 Andrew Ayer + * + * This file is part of git-crypt. + * + * git-crypt is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * git-crypt is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with git-crypt. If not, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify the Program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, the licensors of the Program + * grant you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#include "key.hpp" +#include "util.hpp" +#include "crypto.hpp" +#include +#include +#include +#include +#include +#include +#include + +void Key_file::Entry::load (std::istream& in) +{ + // First comes the AES key + in.read(reinterpret_cast(aes_key), AES_KEY_LEN); + if (in.gcount() != AES_KEY_LEN) { + throw Malformed(); + } + + // Then the HMAC key + in.read(reinterpret_cast(hmac_key), HMAC_KEY_LEN); + if (in.gcount() != HMAC_KEY_LEN) { + throw Malformed(); + } +} + +void Key_file::Entry::store (std::ostream& out) const +{ + out.write(reinterpret_cast(aes_key), AES_KEY_LEN); + out.write(reinterpret_cast(hmac_key), HMAC_KEY_LEN); +} + +void Key_file::Entry::generate () +{ + random_bytes(aes_key, AES_KEY_LEN); + random_bytes(hmac_key, HMAC_KEY_LEN); +} + +const Key_file::Entry* Key_file::get_latest () const +{ + return is_filled() ? get(latest()) : 0; +} + +const Key_file::Entry* Key_file::get (uint32_t version) const +{ + Map::const_iterator it(entries.find(version)); + return it != entries.end() ? &it->second : 0; +} + +void Key_file::add (uint32_t version, const Entry& entry) +{ + entries[version] = entry; +} + + +void Key_file::load_legacy (std::istream& in) +{ + entries[0].load(in); +} + +void Key_file::load (std::istream& in) +{ + unsigned char preamble[16]; + in.read(reinterpret_cast(preamble), 16); + if (in.gcount() != 16) { + throw Malformed(); + } + if (std::memcmp(preamble, "\0GITCRYPTKEY", 12) != 0) { + throw Malformed(); + } + if (load_be32(preamble + 12) != FORMAT_VERSION) { + throw Incompatible(); + } + while (in.peek() != -1) { + uint32_t version; + if (!read_be32(in, version)) { + throw Malformed(); + } + entries[version].load(in); + } +} + +void Key_file::store (std::ostream& out) const +{ + out.write("\0GITCRYPTKEY", 12); + write_be32(out, FORMAT_VERSION); + for (Map::const_iterator it(entries.begin()); it != entries.end(); ++it) { + write_be32(out, it->first); + it->second.store(out); + } +} + +bool Key_file::load (const char* key_file_name) +{ + std::ifstream key_file_in(key_file_name, std::fstream::binary); + if (!key_file_in) { + return false; + } + load(key_file_in); + return true; +} + +bool Key_file::store (const char* key_file_name) const +{ + mode_t old_umask = umask(0077); // make sure key file is protected + std::ofstream key_file_out(key_file_name, std::fstream::binary); + umask(old_umask); + if (!key_file_out) { + return false; + } + store(key_file_out); + key_file_out.close(); + if (!key_file_out) { + return false; + } + return true; +} + +void Key_file::generate () +{ + entries[is_empty() ? 0 : latest() + 1].generate(); +} + +uint32_t Key_file::latest () const +{ + if (is_empty()) { + throw std::invalid_argument("Key_file::latest"); + } + return entries.begin()->first; +} + diff --git a/key.hpp b/key.hpp new file mode 100644 index 0000000..db49154 --- /dev/null +++ b/key.hpp @@ -0,0 +1,84 @@ +/* + * Copyright 2014 Andrew Ayer + * + * This file is part of git-crypt. + * + * git-crypt is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * git-crypt is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with git-crypt. If not, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify the Program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, the licensors of the Program + * grant you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#ifndef _KEY_H +#define _KEY_H + +#include +#include +#include +#include + +enum { + HMAC_KEY_LEN = 64, + AES_KEY_LEN = 32 +}; + +struct Key_file { +public: + struct Entry { + unsigned char aes_key[AES_KEY_LEN]; + unsigned char hmac_key[HMAC_KEY_LEN]; + + void load (std::istream&); + void store (std::ostream&) const; + void generate (); + }; + + struct Malformed { }; // exception class + struct Incompatible { }; // exception class + + const Entry* get_latest () const; + + const Entry* get (uint32_t version) const; + void add (uint32_t version, const Entry&); + + void load_legacy (std::istream&); + void load (std::istream&); + void store (std::ostream&) const; + + bool load (const char* filename); + bool store (const char* filename) const; + + void generate (); + + bool is_empty () const { return entries.empty(); } + bool is_filled () const { return !is_empty(); } + + uint32_t latest () const; + +private: + typedef std::map > Map; + enum { FORMAT_VERSION = 1 }; + + Map entries; +}; + +#endif diff --git a/util.cpp b/util.cpp index e37d7cc..546b4ab 100644 --- a/util.cpp +++ b/util.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -28,8 +28,10 @@ * as that of the covered work. */ +#include "git-crypt.hpp" #include "util.hpp" #include +#include #include #include #include @@ -40,17 +42,77 @@ #include #include +void mkdir_parent (const std::string& path) +{ + std::string::size_type slash(path.find('/', 1)); + while (slash != std::string::npos) { + std::string prefix(path.substr(0, slash)); + struct stat status; + if (stat(prefix.c_str(), &status) == 0) { + // already exists - make sure it's a directory + if (!S_ISDIR(status.st_mode)) { + throw System_error("mkdir_parent", prefix, ENOTDIR); + } + } else { + if (errno != ENOENT) { + throw System_error("mkdir_parent", prefix, errno); + } + // doesn't exist - mkdir it + if (mkdir(prefix.c_str(), 0777) == -1) { + throw System_error("mkdir", prefix, errno); + } + } + + slash = path.find('/', slash + 1); + } +} + +std::string readlink (const char* pathname) +{ + std::vector buffer(64); + ssize_t len; + + while ((len = ::readlink(pathname, &buffer[0], buffer.size())) == static_cast(buffer.size())) { + // buffer may have been truncated - grow and try again + buffer.resize(buffer.size() * 2); + } + if (len == -1) { + throw System_error("readlink", pathname, errno); + } + + return std::string(buffer.begin(), buffer.begin() + len); +} + +std::string our_exe_path () +{ + try { + return readlink("/proc/self/exe"); + } catch (const System_error&) { + if (argv0[0] == '/') { + // argv[0] starts with / => it's an absolute path + return argv0; + } else if (std::strchr(argv0, '/')) { + // argv[0] contains / => it a relative path that should be resolved + char* resolved_path_p = realpath(argv0, NULL); + std::string resolved_path(resolved_path_p); + free(resolved_path_p); + return resolved_path; + } else { + // argv[0] is just a bare filename => not much we can do + return argv0; + } + } +} + int exec_command (const char* command, std::ostream& output) { int pipefd[2]; if (pipe(pipefd) == -1) { - perror("pipe"); - std::exit(9); + throw System_error("pipe", "", errno); } pid_t child = fork(); if (child == -1) { - perror("fork"); - std::exit(9); + throw System_error("fork", "", errno); } if (child == 0) { close(pipefd[0]); @@ -59,7 +121,8 @@ int exec_command (const char* command, std::ostream& output) close(pipefd[1]); } execl("/bin/sh", "sh", "-c", command, NULL); - exit(-1); + perror("/bin/sh"); + _exit(-1); } close(pipefd[1]); char buffer[1024]; @@ -67,49 +130,53 @@ int exec_command (const char* command, std::ostream& output) while ((bytes_read = read(pipefd[0], buffer, sizeof(buffer))) > 0) { output.write(buffer, bytes_read); } + if (bytes_read == -1) { + int read_errno = errno; + close(pipefd[0]); + throw System_error("read", "", read_errno); + } close(pipefd[0]); int status = 0; - waitpid(child, &status, 0); + if (waitpid(child, &status, 0) == -1) { + throw System_error("waitpid", "", errno); + } return status; } -std::string resolve_path (const char* path) +bool successful_exit (int status) { - char* resolved_path_p = realpath(path, NULL); - std::string resolved_path(resolved_path_p); - free(resolved_path_p); - return resolved_path; + return WIFEXITED(status) && WEXITSTATUS(status) == 0; } void open_tempfile (std::fstream& file, std::ios_base::openmode mode) { - const char* tmpdir = getenv("TMPDIR"); - size_t tmpdir_len; - if (tmpdir) { - tmpdir_len = strlen(tmpdir); - } else { + const char* tmpdir = getenv("TMPDIR"); + size_t tmpdir_len = tmpdir ? std::strlen(tmpdir) : 0; + if (tmpdir_len == 0 || tmpdir_len > 4096) { + // no $TMPDIR or it's excessively long => fall back to /tmp tmpdir = "/tmp"; tmpdir_len = 4; } - char* path = new char[tmpdir_len + 18]; - strcpy(path, tmpdir); - strcpy(path + tmpdir_len, "/git-crypt.XXXXXX"); - mode_t old_umask = umask(0077); - int fd = mkstemp(path); + std::vector path_buffer(tmpdir_len + 18); + char* path = &path_buffer[0]; + std::strcpy(path, tmpdir); + std::strcpy(path + tmpdir_len, "/git-crypt.XXXXXX"); + mode_t old_umask = umask(0077); + int fd = mkstemp(path); if (fd == -1) { - perror("mkstemp"); - std::exit(9); + int mkstemp_errno = errno; + umask(old_umask); + throw System_error("mkstemp", "", mkstemp_errno); } umask(old_umask); file.open(path, mode); if (!file.is_open()) { - perror("open"); unlink(path); - std::exit(9); + close(fd); + throw System_error("std::fstream::open", path, 0); } unlink(path); close(fd); - delete[] path; } std::string escape_shell_arg (const std::string& str) diff --git a/util.hpp b/util.hpp index 2bd7356..37fa523 100644 --- a/util.hpp +++ b/util.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -36,8 +36,19 @@ #include #include +struct System_error { + std::string action; + std::string target; + int error; + + System_error (const std::string& a, const std::string& t, int e) : action(a), target(t), error(e) { } +}; + +void mkdir_parent (const std::string& path); // Create parent directories of path, __but not path itself__ +std::string readlink (const char* pathname); +std::string our_exe_path (); int exec_command (const char* command, std::ostream& output); -std::string resolve_path (const char* path); +bool successful_exit (int status); void open_tempfile (std::fstream&, std::ios_base::openmode); std::string escape_shell_arg (const std::string&); uint32_t load_be32 (const unsigned char*);