Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't encrypt empty files in new repositories #210

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion commands.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,25 @@ static std::pair<std::string, std::string> 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<std::string> 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<char*>(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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
9 changes: 9 additions & 0 deletions key.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
Expand Down
7 changes: 6 additions & 1 deletion key.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint32_t, Entry, std::greater<uint32_t> > 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,
Expand Down