diff --git a/commands.cpp b/commands.cpp index 81c401d..7d33c68 100644 --- a/commands.cpp +++ b/commands.cpp @@ -461,6 +461,25 @@ static std::pair get_file_attributes (const std::strin return std::make_pair(filter_attr, diff_attr); } +static bool check_if_blob_is_empty (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?"); + } + + return output.get() == std::stringstream::traits_type::eof(); +} + static bool check_if_blob_is_encrypted (const std::string& object_id) { // git cat-file blob object_id @@ -770,6 +789,10 @@ int clean (int argc, const char** argv) return 1; } + if (file_size == 0 && key_file.get_skip_empty()) { + return 0; + } + // 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 @@ -887,6 +910,11 @@ 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() == 0 && key_file.get_skip_empty()) { + return 0; + } + 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; @@ -991,6 +1019,7 @@ int init (int argc, const char** argv) std::clog << "Generating key..." << std::endl; Key_file key_file; key_file.set_key_name(key_name); + key_file.set_skip_empty(true); key_file.generate(); mkdir_parent(internal_key_path); @@ -1425,6 +1454,7 @@ int keygen (int argc, const char** argv) std::clog << "Generating key..." << std::endl; Key_file key_file; + key_file.set_skip_empty(true); key_file.generate(); if (std::strcmp(key_file_name, "-") == 0) { @@ -1629,7 +1659,8 @@ int status (int argc, const char** argv) 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 the file is empty, don't consider it unencrypted, because in newly-initialized repos (specifically those with keys with skip_empty set) we don't encrypt empty files. Unfortunately, we can't easily determine here if the key has skip_empty set, so just act like it is. This means we won't notice if an old repo has an empty unencrypted file that should be encrypted. Fortunately, this isn't really a big deal because empty files obviously don't contain anything sensitive in them. + const bool blob_is_unencrypted = !object_id.empty() && !check_if_blob_is_encrypted(object_id) && !check_if_blob_is_empty(object_id); if (fix_problems && blob_is_unencrypted) { if (access(filename.c_str(), F_OK) != 0) { diff --git a/key.cpp b/key.cpp index d8fa482..082eb79 100644 --- a/key.cpp +++ b/key.cpp @@ -232,6 +232,11 @@ void Key_file::load_header (std::istream& in) key_name.clear(); throw Malformed(); } + } else if (field_id == HEADER_FIELD_SKIP_EMPTY) { + if (field_len != 0) { + throw Malformed(); + } + skip_empty = true; } else if (field_id & 1) { // unknown critical field throw Incompatible(); } else { @@ -256,6 +261,10 @@ void Key_file::store (std::ostream& out) const write_be32(out, key_name.size()); out.write(key_name.data(), key_name.size()); } + if (skip_empty) { + write_be32(out, HEADER_FIELD_SKIP_EMPTY); + write_be32(out, 0); + } write_be32(out, HEADER_FIELD_END); for (Map::const_iterator it(entries.begin()); it != entries.end(); ++it) { it->second.store(out); diff --git a/key.hpp b/key.hpp index 2695581..557ec2e 100644 --- a/key.hpp +++ b/key.hpp @@ -83,18 +83,23 @@ struct Key_file { 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(); } + + void set_skip_empty (bool v) { skip_empty = v; } + bool get_skip_empty () const { return skip_empty; } private: typedef std::map > Map; enum { FORMAT_VERSION = 2 }; Map entries; std::string key_name; + bool skip_empty = false; void load_header (std::istream&); enum { HEADER_FIELD_END = 0, - HEADER_FIELD_KEY_NAME = 1 + HEADER_FIELD_KEY_NAME = 1, + HEADER_FIELD_SKIP_EMPTY = 3 // If this field is present, empty files are left unencrypted (see issue #53) }; enum { KEY_FIELD_END = 0,