From b6f4f33ba969cfd44cdd9a5df2048ad5b6330fb0 Mon Sep 17 00:00:00 2001 From: Jakub Jirutka Date: Sun, 28 Jul 2019 17:14:09 +0200 Subject: [PATCH 1/2] Change clean() and smudge() functions to accept in/out stream This is a preparation for the merge command. I've extracted this change from #107. Co-Authored-By: Shlomo Shachar Patch-Source: https://github.com/AGWA/git-crypt/pull/180 --- commands.cpp | 38 +++++++++++++++++++------------------- commands.hpp | 5 +++-- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/commands.cpp b/commands.cpp index d25c4cc..93a840e 100644 --- a/commands.cpp +++ b/commands.cpp @@ -690,8 +690,8 @@ static int parse_plumbing_options (const char** key_name, const char** key_file, return parse_options(options, argc, argv); } -// Encrypt contents of stdin and write to stdout -int clean (int argc, const char** argv) +// Encrypt contents of &in and write to &out +int clean (int argc, const char** argv, std::istream& in, std::ostream& out) { const char* key_name = 0; const char* key_path = 0; @@ -724,10 +724,10 @@ int clean (int argc, const char** argv) char buffer[1024]; - while (std::cin && file_size < Aes_ctr_encryptor::MAX_CRYPT_BYTES) { - std::cin.read(buffer, sizeof(buffer)); + while (in && file_size < Aes_ctr_encryptor::MAX_CRYPT_BYTES) { + in.read(buffer, sizeof(buffer)); - const size_t bytes_read = std::cin.gcount(); + const size_t bytes_read = in.gcount(); hmac.add(reinterpret_cast(buffer), bytes_read); file_size += bytes_read; @@ -775,8 +775,8 @@ int clean (int argc, const char** argv) 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), Aes_ctr_encryptor::NONCE_LEN); // ...includes the nonce + out.write("\0GITCRYPT\0", 10); // ...identifies this as an encrypted file + out.write(reinterpret_cast(digest), Aes_ctr_encryptor::NONCE_LEN); // ...includes the nonce // Now encrypt the file and write to stdout Aes_ctr_encryptor aes(key->aes_key, digest); @@ -787,7 +787,7 @@ int clean (int argc, const char** argv) 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); + out.write(buffer, buffer_len); file_data += buffer_len; file_data_len -= buffer_len; } @@ -803,14 +803,14 @@ int clean (int argc, const char** argv) aes.process(reinterpret_cast(buffer), reinterpret_cast(buffer), buffer_len); - std::cout.write(buffer, buffer_len); + out.write(buffer, buffer_len); } } return 0; } -static int decrypt_file_to_stdout (const Key_file& key_file, const unsigned char* header, std::istream& in) +static int decrypt_file_to_stream (const Key_file& key_file, const unsigned char* header, std::istream& in, std::ostream& out = std::cout) { const unsigned char* nonce = header + 10; uint32_t key_version = 0; // TODO: get the version from the file header @@ -828,7 +828,7 @@ static int decrypt_file_to_stdout (const Key_file& key_file, const unsigned char 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()); + out.write(reinterpret_cast(buffer), in.gcount()); } unsigned char digest[Hmac_sha1_state::LEN]; @@ -844,8 +844,8 @@ static int decrypt_file_to_stdout (const Key_file& key_file, const unsigned char return 0; } -// Decrypt contents of stdin and write to stdout -int smudge (int argc, const char** argv) +// Decrypt contents of &in and write to &out +int smudge (int argc, const char** argv, std::istream& in, std::ostream& out) { const char* key_name = 0; const char* key_path = 0; @@ -864,8 +864,8 @@ int smudge (int argc, const char** argv) // Read the header to get the nonce and make sure it's actually encrypted unsigned char header[10 + Aes_ctr_decryptor::NONCE_LEN]; - std::cin.read(reinterpret_cast(header), sizeof(header)); - if (std::cin.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) { + 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::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; @@ -873,12 +873,12 @@ int smudge (int argc, const char** argv) 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(); + out.write(reinterpret_cast(header), in.gcount()); // include the bytes which we already read + out << in.rdbuf(); return 0; } - return decrypt_file_to_stdout(key_file, header, std::cin); + return decrypt_file_to_stream(key_file, header, in, out); } int diff (int argc, const char** argv) @@ -920,7 +920,7 @@ int diff (int argc, const char** argv) } // Go ahead and decrypt it - return decrypt_file_to_stdout(key_file, header, in); + return decrypt_file_to_stream(key_file, header, in); } void help_init (std::ostream& out) diff --git a/commands.hpp b/commands.hpp index f441e93..bf4632c 100644 --- a/commands.hpp +++ b/commands.hpp @@ -33,6 +33,7 @@ #include #include +#include struct Error { std::string message; @@ -41,8 +42,8 @@ struct Error { }; // Plumbing commands: -int clean (int argc, const char** argv); -int smudge (int argc, const char** argv); +int clean (int argc, const char** argv, std::istream& in = std::cin, std::ostream& out = std::cout); +int smudge (int argc, const char** argv, std::istream& in = std::cin, std::ostream& out = std::cout); int diff (int argc, const char** argv); // Public commands: int init (int argc, const char** argv); From b2efa705c4bd8c5d5590816226aace123bf19265 Mon Sep 17 00:00:00 2001 From: Jakub Jirutka Date: Sun, 28 Jul 2019 19:18:39 +0200 Subject: [PATCH 2/2] Add git-crypt merge driver to support secret files merging This commit is based on #107. Co-Authored-By: Shlomo Shachar Patch-Source: https://github.com/AGWA/git-crypt/pull/180 --- README | 8 ++-- README.md | 8 ++-- commands.cpp | 111 +++++++++++++++++++++++++++++++++++++++++++ commands.hpp | 1 + doc/multiple_keys.md | 2 +- git-crypt.cpp | 4 ++ man/git-crypt.xml | 8 ++-- 7 files changed, 129 insertions(+), 13 deletions(-) diff --git a/README b/README index 232947f..2810a26 100644 --- a/README +++ b/README @@ -28,8 +28,8 @@ Configure a repository to use git-crypt: Specify files to encrypt by creating a .gitattributes file: - secretfile filter=git-crypt diff=git-crypt - *.key filter=git-crypt diff=git-crypt + secretfile filter=git-crypt diff=git-crypt merge=git-crypt + *.key filter=git-crypt diff=git-crypt merge=git-crypt Like a .gitignore file, it can match wildcards and should be checked into the repository. See below for more information about .gitattributes. @@ -151,8 +151,8 @@ Also note that the pattern `dir/*` does not match files under sub-directories of dir/. To encrypt an entire sub-tree dir/, place the following in dir/.gitattributes: - * filter=git-crypt diff=git-crypt - .gitattributes !filter !diff + * filter=git-crypt diff=git-crypt merge=git-crypt + .gitattributes !filter !diff !merge The second pattern is essential for ensuring that .gitattributes itself is not encrypted. diff --git a/README.md b/README.md index d24517a..d008aab 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ Configure a repository to use git-crypt: Specify files to encrypt by creating a .gitattributes file: - secretfile filter=git-crypt diff=git-crypt - *.key filter=git-crypt diff=git-crypt + secretfile filter=git-crypt diff=git-crypt merge=git-crypt + *.key filter=git-crypt diff=git-crypt merge=git-crypt Like a .gitignore file, it can match wildcards and should be checked into the repository. See below for more information about .gitattributes. @@ -153,8 +153,8 @@ Also note that the pattern `dir/*` does not match files under sub-directories of dir/. To encrypt an entire sub-tree dir/, place the following in dir/.gitattributes: - * filter=git-crypt diff=git-crypt - .gitattributes !filter !diff + * filter=git-crypt diff=git-crypt merge=git-crypt + .gitattributes !filter !diff !merge The second pattern is essential for ensuring that .gitattributes itself is not encrypted. diff --git a/commands.cpp b/commands.cpp index 93a840e..415b36a 100644 --- a/commands.cpp +++ b/commands.cpp @@ -160,11 +160,16 @@ static void configure_git_filters (const char* 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); + git_config(std::string("merge.git-crypt-") + key_name + ".name", "git-crypt merge driver"); + git_config(std::string("merge.git-crypt-") + key_name + ".driver", + escaped_git_crypt_path + " merge --key-name=" + key_name + " %A %O %B %L"); } 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"); + git_config("merge.git-crypt.name", "git-crypt merge driver"); + git_config("merge.git-crypt.driver", escaped_git_crypt_path + " merge %A %O %B %L"); } } @@ -181,6 +186,12 @@ static void deconfigure_git_filters (const char* key_name) if (git_has_config("diff." + attribute_name(key_name) + ".textconv")) { git_deconfig("diff." + attribute_name(key_name)); } + + if (git_has_config("merge." + attribute_name(key_name) + ".name") || + git_has_config("merge." + attribute_name(key_name) + ".driver")) { + + git_deconfig("merge." + attribute_name(key_name)); + } } static bool git_checkout (const std::vector& paths) @@ -923,6 +934,106 @@ int diff (int argc, const char** argv) return decrypt_file_to_stream(key_file, header, in); } +int merge (int argc, const char** argv) +{ + const char* key_name = 0; // unused but needed + const char* key_path = 0; // unused but needed + const char* current_path = 0; // %A + const char* base_path = 0; // %O + const char* other_path = 0; // %B + const char* marker_size = 0; // %L + + int argi = parse_plumbing_options(&key_name, &key_path, argc, argv); + if (argc - argi == 4) { + current_path = argv[argi]; + base_path = argv[argi + 1]; + other_path = argv[argi + 2]; + marker_size = argv[argi + 3]; + } else { + std::clog << "Usage: git-crypt merge [--key-name=NAME] [--key-file=PATH] CURRENT BASE OTHER MARKER_SIZE" << std::endl; + return 2; + } + + // Run smudge on input files + std::vector smudge_files; + smudge_files.push_back(current_path); + smudge_files.push_back(base_path); + smudge_files.push_back(other_path); + + for (std::vector::const_iterator file(smudge_files.begin()); file != smudge_files.end(); ++file) { + std::ifstream in(*file, std::ifstream::binary); + if (!in) { + std::clog << "git-crypt: " << *file << ": unable to open for reading" << std::endl; + return 1; + } + in.exceptions(std::ifstream::badbit); + + std::ofstream out(*file + ".tmp", std::ofstream::binary | std::ofstream::trunc); + if (!out) { + std::clog << "git-crypt: " << *file << ".tmp: unable to open for writing" << std::endl; + return 1; + } + out.exceptions(std::ifstream::badbit); + + if (smudge(argi, argv, in, out) != 0) { + std::clog << "Error: failed to smudge " << *file << ": unable to merge file" << std::endl; + return 1; + } + in.close(); + out.close(); + } + + // git merge-file --marker-size + std::vector command; + command.push_back("git"); + command.push_back("merge-file"); + command.push_back("-L"); + command.push_back("ours"); + command.push_back("-L"); + command.push_back("base"); + command.push_back("-L"); + command.push_back("theirs"); + command.push_back("--marker-size"); + command.push_back(marker_size); + command.push_back(std::string(current_path) + ".tmp"); + command.push_back(std::string(base_path) + ".tmp"); + command.push_back(std::string(other_path) + ".tmp"); + int ret = exit_status(exec_command(command)); + + // Run clean on output file + // We have to clean (encrypt) the output file because git runs smudge filter on it + // afterwards which would complain about the file not being encrypted. + { + std::ifstream in(std::string(current_path) + ".tmp", std::ifstream::binary); + if (!in) { + std::clog << "git-crypt: " << current_path << ".tmp: unable to open for reading" << std::endl; + return 1; + } + in.exceptions(std::ifstream::badbit); + + std::ofstream out(current_path, std::ofstream::binary | std::ofstream::trunc); + if (!out) { + std::clog << "git-crypt: " << current_path << ": unable to open for writing" << std::endl; + return 1; + } + out.exceptions(std::ifstream::badbit); + + if (clean(argi, argv, in, out) != 0) { + std::clog << "Error: failed to clean " << current_path << ": unable to merge file" << std::endl; + return 1; + } + in.close(); + out.close(); + } + + // Clean-up temporary files + for (std::vector::const_iterator file(smudge_files.begin()); file != smudge_files.end(); ++file) { + remove_file(*file + ".tmp"); + } + + return ret; +} + void help_init (std::ostream& out) { // |--------------------------------------------------------------------------------| 80 chars diff --git a/commands.hpp b/commands.hpp index bf4632c..51f4aea 100644 --- a/commands.hpp +++ b/commands.hpp @@ -45,6 +45,7 @@ struct Error { int clean (int argc, const char** argv, std::istream& in = std::cin, std::ostream& out = std::cout); int smudge (int argc, const char** argv, std::istream& in = std::cin, std::ostream& out = std::cout); int diff (int argc, const char** argv); +int merge (int argc, const char** argv); // Public commands: int init (int argc, const char** argv); int unlock (int argc, const char** argv); diff --git a/doc/multiple_keys.md b/doc/multiple_keys.md index 6d7fc69..66b462a 100644 --- a/doc/multiple_keys.md +++ b/doc/multiple_keys.md @@ -11,7 +11,7 @@ option to `git-crypt init` as follows: To encrypt a file with an alternative key, use the `git-crypt-KEYNAME` filter in `.gitattributes` as follows: - secretfile filter=git-crypt-KEYNAME diff=git-crypt-KEYNAME + secretfile filter=git-crypt-KEYNAME diff=git-crypt-KEYNAME merge=git-crypt-KEYNAME To export an alternative key or share it with a GPG user, pass the `-k KEYNAME` option to `git-crypt export-key` or `git-crypt add-gpg-user` diff --git a/git-crypt.cpp b/git-crypt.cpp index 9505834..d8c2072 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -73,6 +73,7 @@ static void print_usage (std::ostream& out) out << " clean [LEGACY-KEYFILE]" << std::endl; out << " smudge [LEGACY-KEYFILE]" << std::endl; out << " diff [LEGACY-KEYFILE] FILE" << std::endl; + out << " merge" << std::endl; */ out << std::endl; out << "See 'git-crypt help COMMAND' for more information on a specific command." << std::endl; @@ -231,6 +232,9 @@ try { if (std::strcmp(command, "diff") == 0) { return diff(argc, argv); } + if (std::strcmp(command, "merge") == 0) { + return merge(argc, argv); + } } catch (const Option_error& e) { std::clog << "git-crypt: Error: " << e.option_name << ": " << e.message << std::endl; help_for_command(command, std::clog); diff --git a/man/git-crypt.xml b/man/git-crypt.xml index 96f53d7..21e1359 100644 --- a/man/git-crypt.xml +++ b/man/git-crypt.xml @@ -310,11 +310,11 @@ Then, you specify the files to encrypt by creating a gitattributes5 file. - Each file which you want to encrypt should be assigned the "filter=git-crypt diff=git-crypt" + Each file which you want to encrypt should be assigned the "filter=git-crypt diff=git-crypt merge=git-crypt" attributes. For example: - secretfile filter=git-crypt diff=git-crypt *.key filter=git-crypt diff=git-crypt + secretfile filter=git-crypt diff=git-crypt merge=git-crypt *.key filter=git-crypt diff=git-crypt merge=git-crypt Like a .gitignore file, .gitattributes files can match wildcards and @@ -383,7 +383,7 @@ following in dir/.gitattributes: - * filter=git-crypt diff=git-crypt .gitattributes !filter !diff + * filter=git-crypt diff=git-crypt merge=git-crypt .gitattributes !filter !diff !merge The second pattern is essential for ensuring that .gitattributes itself @@ -414,7 +414,7 @@ filter in .gitattributes as follows: - secretfile filter=git-crypt-KEYNAME diff=git-crypt-KEYNAME + secretfile filter=git-crypt-KEYNAME diff=git-crypt-KEYNAME merge=git-crypt-KEYNAME To export an alternative key or share it with a GPG user, pass the