From 01f152b746a9d64c1d0fde930d9a4f9bee770fd0 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Wed, 23 Jul 2014 19:58:13 -0700 Subject: [PATCH] Check HMAC in smudge and diff commands Git-crypt's position has always been that authentication is best left to Git, since 1) Git provides immutable history based on SHA-1 hashes as well as GPG-signed commits and tags, and 2) git-crypt can't be used safely anyways unless the overall integrity of your repository is assured. But, since git-crypt already has easy access to a (truncated) HMAC of the file when decrypting, there's really no reason why git-crypt shouldn't just verify it and provide an additional layer of protection. --- commands.cpp | 21 ++++++++++++++++++++- util.cpp | 17 +++++++++++++++++ util.hpp | 1 + 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/commands.cpp b/commands.cpp index 2ed5254..0e5f7df 100644 --- a/commands.cpp +++ b/commands.cpp @@ -532,7 +532,26 @@ static int decrypt_file_to_stdout (const Key_file& key_file, const unsigned char return 1; } - Aes_ctr_decryptor::process_stream(in, std::cout, key->aes_key, nonce); + Aes_ctr_decryptor aes(key->aes_key, nonce); + Hmac_sha1_state hmac(key->hmac_key, HMAC_KEY_LEN); + while (in) { + unsigned char buffer[1024]; + 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()); + } + + unsigned char digest[Hmac_sha1_state::LEN]; + hmac.get(digest); + if (!leakless_equals(digest, nonce, Aes_ctr_decryptor::NONCE_LEN)) { + std::clog << "git-crypt: error: encrypted file has been tampered with!" << std::endl; + // Although we've already written the tampered file to stdout, exiting + // with a non-zero status will tell git the file has not been filtered, + // so git will not replace it. + return 1; + } + return 0; } diff --git a/util.cpp b/util.cpp index 189e52a..2da0622 100644 --- a/util.cpp +++ b/util.cpp @@ -92,6 +92,23 @@ void* explicit_memset (void* s, int c, std::size_t n) return s; } +static bool leakless_equals_char (const unsigned char* a, const unsigned char* b, std::size_t len) +{ + volatile int diff = 0; + + while (len > 0) { + diff |= *a++ ^ *b++; + --len; + } + + return diff == 0; +} + +bool leakless_equals (const void* a, const void* b, std::size_t len) +{ + return leakless_equals_char(reinterpret_cast(a), reinterpret_cast(b), len); +} + static void init_std_streams_platform (); // platform-specific initialization void init_std_streams () diff --git a/util.hpp b/util.hpp index e79d805..8281294 100644 --- a/util.hpp +++ b/util.hpp @@ -71,6 +71,7 @@ void store_be32 (unsigned char*, uint32_t); bool read_be32 (std::istream& in, uint32_t&); void write_be32 (std::ostream& out, uint32_t); void* explicit_memset (void* s, int c, size_t n); // memset that won't be optimized away +bool leakless_equals (const void* a, const void* b, size_t len); // compare bytes w/o leaking timing void init_std_streams (); mode_t util_umask (mode_t); int util_rename (const char*, const char*);