mirror of
https://github.com/AGWA/git-crypt.git
synced 2025-12-05 20:40:05 -08:00
Initial version
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.o
|
||||
git-crypt
|
||||
15
Makefile
Normal file
15
Makefile
Normal file
@@ -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
|
||||
198
commands.cpp
Normal file
198
commands.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
#include "commands.hpp"
|
||||
#include "crypto.hpp"
|
||||
#include "util.hpp"
|
||||
#include <stdint.h>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
|
||||
// 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<const uint8_t*>(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<char*>(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<uint8_t*>(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<uint8_t*>(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<uint8_t*>(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));
|
||||
}
|
||||
12
commands.hpp
Normal file
12
commands.hpp
Normal file
@@ -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
|
||||
|
||||
85
crypto.cpp
Normal file
85
crypto.cpp
Normal file
@@ -0,0 +1,85 @@
|
||||
#define _BSD_SOURCE
|
||||
#include "crypto.hpp"
|
||||
#include <openssl/aes.h>
|
||||
#include <openssl/sha.h>
|
||||
#include <openssl/hmac.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <endian.h>
|
||||
|
||||
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<uint8_t*>(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<char*>(buffer), sizeof(buffer));
|
||||
state.process_block(enc_key, buffer, buffer, in.gcount());
|
||||
out.write(reinterpret_cast<char*>(buffer), in.gcount());
|
||||
}
|
||||
}
|
||||
39
crypto.hpp
Normal file
39
crypto.hpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#ifndef _CRYPTO_H
|
||||
#define _CRYPTO_H
|
||||
|
||||
#include <openssl/aes.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <iosfwd>
|
||||
|
||||
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
|
||||
51
git-crypt.cpp
Normal file
51
git-crypt.cpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#include "commands.hpp"
|
||||
#include "util.hpp"
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
51
util.cpp
Normal file
51
util.cpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#include "util.hpp"
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user