commit 6e3dd5a8d312a6f5e7e4e70cdac77a1ffd6113da Author: Andrew Ayer Date: Fri Jul 6 15:38:40 2012 -0700 Initial version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81afb4a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.o +git-crypt diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0664387 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +CXX := g++ +CXXFLAGS := -Wall -pedantic -ansi -Wno-long-long -O2 +LDFLAGS := -lcrypto + +OBJFILES = git-crypt.o commands.o crypto.o util.o + +all: git-crypt + +git-crypt: $(OBJFILES) + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) + +clean: + rm -f *.o git-crypt + +.PHONY: all clean diff --git a/commands.cpp b/commands.cpp new file mode 100644 index 0000000..fb14fa2 --- /dev/null +++ b/commands.cpp @@ -0,0 +1,198 @@ +#include "commands.hpp" +#include "crypto.hpp" +#include "util.hpp" +#include +#include +#include +#include +#include +#include +#include + +// Encrypt contents of stdin and write to stdout +void clean (const char* keyfile) +{ + keys_t keys; + load_keys(keyfile, &keys); + + // First read the entire file into a buffer (TODO: if the buffer gets big, use a temp file instead) + std::string file_contents; + char buffer[1024]; + while (std::cin) { + std::cin.read(buffer, sizeof(buffer)); + file_contents.append(buffer, std::cin.gcount()); + } + const uint8_t* file_data = reinterpret_cast(file_contents.data()); + size_t file_len = file_contents.size(); + + // Make sure the file isn't so large we'll overflow the counter value (which would doom security) + if (file_len > MAX_CRYPT_BYTES) { + std::clog << "File too long to encrypt securely\n"; + std::exit(1); + } + + // Compute an HMAC of the file to use as the encryption nonce. 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 hasn't. Although this is not semantically secure under CPA, this still has some + // nice properties. For instance, if a file changes just a tiny bit, the resulting ciphertext will + // be completely different, leaking no information. Also, since we're using the output from a + // secure hash function plus a counter as the input to our block cipher, we should never have a situation + // where two different plaintext blocks get encrypted with the same CTR value. A nonce will be reused + // only if the entire file is the same, which leaks no information except that the files are the same. + // + // To prevent an attacker from building a dictionary of hash values and then looking up the + // nonce, which must be stored in the clear, to decrypt the ciphertext, we use an HMAC + // as opposed to a straight hash. + uint8_t digest[12]; + hmac_sha1_96(digest, file_data, file_len, keys.hmac, HMAC_KEY_LEN); + + // Write a header that: + std::cout.write("\0GITCRYPT\0", 10); // identifies this as an encrypted file + std::cout.write(reinterpret_cast(digest), 12); // includes the nonce + + // Now encrypt the file and write to stdout + aes_ctr_state state(digest, 12); + for (size_t i = 0; i < file_len; i += sizeof(buffer)) { + size_t block_len = std::min(sizeof(buffer), file_len - i); + state.process_block(&keys.enc, file_data + i, reinterpret_cast(buffer), block_len); + std::cout.write(buffer, block_len); + } +} + +// Decrypt contents of stdin and write to stdout +void smudge (const char* keyfile) +{ + keys_t keys; + load_keys(keyfile, &keys); + + // 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); + } + + process_stream(std::cin, std::cout, &keys.enc, reinterpret_cast(header + 10)); +} + +void diff (const char* keyfile, const char* filename) +{ + keys_t keys; + load_keys(keyfile, &keys); + + // Open the file + std::ifstream in(filename); + if (!in) { + perror(filename); + std::exit(1); + } + + // 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) { + // 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; + } + + process_stream(in, std::cout, &keys.enc, reinterpret_cast(header + 10)); +} + + +void init (const char* argv0, const char* keyfile) +{ + if (access(keyfile, R_OK) == -1) { + perror(keyfile); + std::exit(1); + } + + // 1. Make sure working directory is clean + int status; + std::string status_output; + status = exec_command("git status --porcelain", status_output); + if (status != 0) { + std::clog << "git status failed - is this a git repository?\n"; + std::exit(1); + } else if (!status_output.empty()) { + std::clog << "Working directory not clean.\n"; + std::exit(1); + } + + std::string git_crypt_path(std::strchr(argv0, '/') ? resolve_path(argv0) : argv0); + std::string keyfile_path(resolve_path(keyfile)); + + + // 2. Add config options to git + + // git config --add filter.git-crypt.smudge "git-crypt smudge /path/to/key" + std::string command("git config --add filter.git-crypt.smudge \""); + command += git_crypt_path; + command += " smudge "; + command += keyfile_path; + command += "\""; + + if (system(command.c_str()) != 0) { + std::clog << "git config failed\n"; + std::exit(1); + } + + // git config --add filter.git-crypt.clean "git-crypt clean /path/to/key" + command = "git config --add filter.git-crypt.clean \""; + command += git_crypt_path; + command += " clean "; + command += keyfile_path; + command += "\""; + + if (system(command.c_str()) != 0) { + std::clog << "git config failed\n"; + std::exit(1); + } + + // git config --add diff.git-crypt.textconv "git-crypt diff /path/to/key" + command = "git config --add diff.git-crypt.textconv \""; + command += git_crypt_path; + command += " diff "; + command += keyfile_path; + command += "\""; + + if (system(command.c_str()) != 0) { + std::clog << "git config failed\n"; + std::exit(1); + } + + + // 3. Do a hard reset so any files that were previously checked out encrypted + // will now be checked out decrypted. + if (system("git reset --hard") != 0) { + std::clog << "git reset --hard failed\n"; + std::exit(1); + } +} + +void keygen (const char* keyfile) +{ + std::ofstream keyout(keyfile); + if (!keyout) { + perror(keyfile); + std::exit(1); + } + std::ifstream randin("/dev/random"); + if (!randin) { + perror("/dev/random"); + std::exit(1); + } + char buffer[AES_KEY_BITS/8 + HMAC_KEY_LEN]; + randin.read(buffer, sizeof(buffer)); + if (randin.gcount() != sizeof(buffer)) { + std::clog << "Premature end of random data.\n"; + std::exit(1); + } + keyout.write(buffer, sizeof(buffer)); +} diff --git a/commands.hpp b/commands.hpp new file mode 100644 index 0000000..f36ee43 --- /dev/null +++ b/commands.hpp @@ -0,0 +1,12 @@ +#ifndef _COMMANDS_H +#define _COMMANDS_H + + +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); + +#endif + diff --git a/crypto.cpp b/crypto.cpp new file mode 100644 index 0000000..d562bd8 --- /dev/null +++ b/crypto.cpp @@ -0,0 +1,85 @@ +#define _BSD_SOURCE +#include "crypto.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void load_keys (const char* filepath, keys_t* keys) +{ + 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))); + byte_counter = 0; + memset(otp, '\0', sizeof(otp)); +} + +void aes_ctr_state::process_block (const AES_KEY* key, const uint8_t* in, uint8_t* 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 = htole32(byte_counter / 16); + memcpy(ctr, nonce, 12); + memcpy(ctr + 12, &blockno, 4); + AES_encrypt(ctr, otp, key); + } + + // encrypt one byte + out[i] = in[i] ^ otp[byte_counter++ % 16]; + } +} + +// Compute HMAC-SHA1-96 (i.e. first 96 bits of HMAC-SHA1) for the given buffer with the given key +void hmac_sha1_96 (uint8_t* out, const uint8_t* buffer, size_t buffer_len, const uint8_t* key, size_t key_len) +{ + uint8_t full_digest[20]; + HMAC(EVP_sha1(), key, key_len, buffer, buffer_len, full_digest, NULL); + memcpy(out, full_digest, 12); // Truncate to first 96 bits +} + +// 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) +{ + aes_ctr_state state(nonce, 12); + + uint8_t buffer[1024]; + while (in) { + in.read(reinterpret_cast(buffer), sizeof(buffer)); + state.process_block(enc_key, buffer, buffer, in.gcount()); + out.write(reinterpret_cast(buffer), in.gcount()); + } +} diff --git a/crypto.hpp b/crypto.hpp new file mode 100644 index 0000000..97ae2da --- /dev/null +++ b/crypto.hpp @@ -0,0 +1,39 @@ +#ifndef _CRYPTO_H +#define _CRYPTO_H + +#include +#include +#include +#include + +enum { + 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 keys_t { + AES_KEY enc; + uint8_t hmac[HMAC_KEY_LEN]; +}; +void load_keys (const char* filepath, keys_t* keys); + +class aes_ctr_state { + char nonce[12]; // 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 + +public: + aes_ctr_state (const uint8_t* arg_nonce, size_t arg_nonce_len); + + void process_block (const AES_KEY* key, const uint8_t* in, uint8_t* out, size_t len); +}; + +// Compute HMAC-SHA1-96 (i.e. first 96 bits of HMAC-SHA1) for the given buffer with the given key +void hmac_sha1_96 (uint8_t* out, const uint8_t* buffer, size_t buffer_len, const uint8_t* key, size_t key_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); + + +#endif diff --git a/git-crypt.cpp b/git-crypt.cpp new file mode 100644 index 0000000..c3bb629 --- /dev/null +++ b/git-crypt.cpp @@ -0,0 +1,51 @@ +#include "commands.hpp" +#include "util.hpp" +#include +#include + +static void print_usage (const char* argv0) +{ + 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"; +} + + +int main (int argc, const char** argv) +{ + // The following two lines are essential for achieving good performance: + std::ios_base::sync_with_stdio(false); + std::cin.tie(0); + + if (argc < 3) { + print_usage(argv[0]); + return 2; + } + + + 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; + } + + return 0; +} + + diff --git a/util.cpp b/util.cpp new file mode 100644 index 0000000..c848dc4 --- /dev/null +++ b/util.cpp @@ -0,0 +1,51 @@ +#include "util.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +int exec_command (const char* command, std::string& 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.append(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; +} + diff --git a/util.hpp b/util.hpp new file mode 100644 index 0000000..2d79593 --- /dev/null +++ b/util.hpp @@ -0,0 +1,10 @@ +#ifndef _UTIL_H +#define _UTIL_H + +#include + +int exec_command (const char* command, std::string& output); +std::string resolve_path (const char* path); + +#endif +