2 Commits

Author SHA1 Message Date
Andrew Ayer
e4f73bf3b0 status: never assume empty blobs are unencrypted
See comment in source code for rationale.
2020-07-29 09:23:03 -04:00
Andrew Ayer
8ba75c4719 Don't encrypt empty files in new repositories
git has several problems with using smudge/clean filters
on empty files (see issue #53).  The easiest fix is to
just not encrypt empty files. Since it was already obvious
from the encrypted file length that a file was empty, skipping
empty files does not decrease security.

Since skipping empty files is a breaking change to the
git-crypt file format, we only do this on new repositories.
Specifically, we add a new critical header field to the key
file called skip_empty which is set in new keys.  We
skip empty files if and only if this field is present.

Closes: #53
Closes: #162
2020-07-29 08:57:22 -04:00
12 changed files with 75 additions and 122 deletions

View File

@@ -1,46 +0,0 @@
on:
release:
types: [published]
name: Build Release Binary (Linux)
jobs:
build:
name: Build Release Binary
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install dependencies
run: sudo apt install libssl-dev
- name: Build binary
run: make
- name: Upload release artifact
uses: actions/upload-artifact@v3
with:
name: git-crypt-artifacts
path: git-crypt
upload:
name: Upload Release Binary
runs-on: ubuntu-latest
needs: build
permissions:
contents: write
steps:
- name: Download release artifact
uses: actions/download-artifact@v3
with:
name: git-crypt-artifacts
- name: Upload release asset
uses: actions/github-script@v3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require("fs").promises;
const { repo: { owner, repo }, sha } = context;
await github.repos.uploadReleaseAsset({
owner, repo,
release_id: ${{ github.event.release.id }},
name: 'git-crypt-${{ github.event.release.name }}-linux-x86_64',
data: await fs.readFile('git-crypt'),
});

View File

@@ -1,56 +0,0 @@
on:
release:
types: [published]
name: Build Release Binary (Windows)
jobs:
build:
name: Build Release Binary
runs-on: windows-2022
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup msys2
uses: msys2/setup-msys2@v2
with:
msystem: MINGW64
update: true
install: >-
base-devel
msys2-devel
mingw-w64-x86_64-toolchain
mingw-w64-x86_64-openssl
openssl-devel
- name: Build binary
shell: msys2 {0}
run: make LDFLAGS="-static-libstdc++ -static -lcrypto -lws2_32"
- name: Upload release artifact
uses: actions/upload-artifact@v3
with:
name: git-crypt-artifacts
path: git-crypt.exe
upload:
name: Upload Release Binary
runs-on: ubuntu-latest
needs: build
permissions:
contents: write
steps:
- name: Download release artifact
uses: actions/download-artifact@v3
with:
name: git-crypt-artifacts
- name: Upload release asset
uses: actions/github-script@v3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require("fs").promises;
const { repo: { owner, repo }, sha } = context;
await github.repos.uploadReleaseAsset({
owner, repo,
release_id: ${{ github.event.release.id }},
name: 'git-crypt-${{ github.event.release.name }}-x86_64.exe',
data: await fs.readFile('git-crypt.exe'),
});

View File

@@ -4,7 +4,8 @@ documentation, bug reports, or anything else that improves git-crypt.
When contributing code, please consider the following guidelines: When contributing code, please consider the following guidelines:
* You are encouraged to open an issue on GitHub to discuss any non-trivial * You are encouraged to open an issue on GitHub or send mail to
git-crypt-discuss@lists.cloudmutt.com to discuss any non-trivial
changes before you start coding. changes before you start coding.
* Please mimic the existing code style as much as possible. In * Please mimic the existing code style as much as possible. In
@@ -14,7 +15,8 @@ When contributing code, please consider the following guidelines:
* To minimize merge commits, please rebase your changes before opening * To minimize merge commits, please rebase your changes before opening
a pull request. a pull request.
* To submit your patch, open a pull request on GitHub. * To submit your patch, open a pull request on GitHub or send a
properly-formatted patch to git-crypt-discuss@lists.cloudmutt.com.
Finally, be aware that since git-crypt is security-sensitive software, Finally, be aware that since git-crypt is security-sensitive software,
the bar for contributions is higher than average. Please don't be the bar for contributions is higher than average. Please don't be

5
NEWS
View File

@@ -1,8 +1,3 @@
v0.7.0 (2022-04-21)
* Avoid "argument list too long" errors on macOS.
* Fix handling of "-" arguments.
* Minor documentation improvements.
v0.6.0 (2017-11-26) v0.6.0 (2017-11-26)
* Add support for OpenSSL 1.1 (still works with OpenSSL 1.0). * Add support for OpenSSL 1.1 (still works with OpenSSL 1.0).
* Switch to C++11 (gcc 4.9 or higher now required to build). * Switch to C++11 (gcc 4.9 or higher now required to build).

View File

@@ -1,11 +1,6 @@
News News
==== ====
######v0.7.0 (2022-04-21)
* Avoid "argument list too long" errors on macOS.
* Fix handling of "-" arguments.
* Minor documentation improvements.
######v0.6.0 (2017-11-26) ######v0.6.0 (2017-11-26)
* Add support for OpenSSL 1.1 (still works with OpenSSL 1.0). * Add support for OpenSSL 1.1 (still works with OpenSSL 1.0).
* Switch to C++11 (gcc 4.9 or higher now required to build). * Switch to C++11 (gcc 4.9 or higher now required to build).

11
README
View File

@@ -70,7 +70,7 @@ encryption and decryption happen transparently.
CURRENT STATUS CURRENT STATUS
The latest version of git-crypt is 0.7.0, released on 2022-04-21. The latest version of git-crypt is 0.6.0, released on 2017-11-26.
git-crypt aims to be bug-free and reliable, meaning it shouldn't git-crypt aims to be bug-free and reliable, meaning it shouldn't
crash, malfunction, or expose your confidential data. However, crash, malfunction, or expose your confidential data. However,
it has not yet reached maturity, meaning it is not as documented, it has not yet reached maturity, meaning it is not as documented,
@@ -158,3 +158,12 @@ match it accidentally. If necessary, you can exclude .gitattributes from
encryption like this: encryption like this:
.gitattributes !filter !diff .gitattributes !filter !diff
MAILING LISTS
To stay abreast of, and provide input to, git-crypt development, consider
subscribing to one or both of our mailing lists:
Announcements: https://lists.cloudmutt.com/mailman/listinfo/git-crypt-announce
Discussion: https://lists.cloudmutt.com/mailman/listinfo/git-crypt-discuss

View File

@@ -71,8 +71,8 @@ encryption and decryption happen transparently.
Current Status Current Status
-------------- --------------
The latest version of git-crypt is [0.7.0](NEWS.md), released on The latest version of git-crypt is [0.6.0](NEWS.md), released on
2022-04-21. git-crypt aims to be bug-free and reliable, meaning it 2017-11-26. git-crypt aims to be bug-free and reliable, meaning it
shouldn't crash, malfunction, or expose your confidential data. shouldn't crash, malfunction, or expose your confidential data.
However, it has not yet reached maturity, meaning it is not as However, it has not yet reached maturity, meaning it is not as
documented, featureful, or easy-to-use as it should be. Additionally, documented, featureful, or easy-to-use as it should be. Additionally,
@@ -160,3 +160,12 @@ match it accidentally. If necessary, you can exclude .gitattributes from
encryption like this: encryption like this:
.gitattributes !filter !diff .gitattributes !filter !diff
Mailing Lists
-------------
To stay abreast of, and provide input to, git-crypt development,
consider subscribing to one or both of our mailing lists:
* [Announcements](https://lists.cloudmutt.com/mailman/listinfo/git-crypt-announce)
* [Discussion](https://lists.cloudmutt.com/mailman/listinfo/git-crypt-discuss)

View File

@@ -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); 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) static bool check_if_blob_is_encrypted (const std::string& object_id)
{ {
// git cat-file blob object_id // git cat-file blob object_id
@@ -770,6 +789,10 @@ int clean (int argc, const char** argv)
return 1; 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. // 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 // 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 // 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 // Read the header to get the nonce and make sure it's actually encrypted
unsigned char header[10 + Aes_ctr_decryptor::NONCE_LEN]; unsigned char header[10 + Aes_ctr_decryptor::NONCE_LEN];
std::cin.read(reinterpret_cast<char*>(header), sizeof(header)); 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) { if (std::cin.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) {
// File not encrypted - just copy it out to stdout // File not encrypted - just copy it out to stdout
std::clog << "git-crypt: Warning: file not encrypted" << std::endl; 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; std::clog << "Generating key..." << std::endl;
Key_file key_file; Key_file key_file;
key_file.set_key_name(key_name); key_file.set_key_name(key_name);
key_file.set_skip_empty(true);
key_file.generate(); key_file.generate();
mkdir_parent(internal_key_path); mkdir_parent(internal_key_path);
@@ -1425,6 +1454,7 @@ int keygen (int argc, const char** argv)
std::clog << "Generating key..." << std::endl; std::clog << "Generating key..." << std::endl;
Key_file key_file; Key_file key_file;
key_file.set_skip_empty(true);
key_file.generate(); key_file.generate();
if (std::strcmp(key_file_name, "-") == 0) { 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) { if (file_attrs.first == "git-crypt" || std::strncmp(file_attrs.first.c_str(), "git-crypt-", 10) == 0) {
// File is encrypted // 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 (fix_problems && blob_is_unencrypted) {
if (access(filename.c_str(), F_OK) != 0) { if (access(filename.c_str(), F_OK) != 0) {

View File

@@ -31,7 +31,7 @@
#ifndef GIT_CRYPT_GIT_CRYPT_HPP #ifndef GIT_CRYPT_GIT_CRYPT_HPP
#define GIT_CRYPT_GIT_CRYPT_HPP #define GIT_CRYPT_GIT_CRYPT_HPP
#define VERSION "0.7.0" #define VERSION "0.6.0"
extern const char* argv0; // initialized in main() to argv[0] extern const char* argv0; // initialized in main() to argv[0]

View File

@@ -232,6 +232,11 @@ void Key_file::load_header (std::istream& in)
key_name.clear(); key_name.clear();
throw Malformed(); 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 } else if (field_id & 1) { // unknown critical field
throw Incompatible(); throw Incompatible();
} else { } else {
@@ -256,6 +261,10 @@ void Key_file::store (std::ostream& out) const
write_be32(out, key_name.size()); write_be32(out, key_name.size());
out.write(key_name.data(), 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); write_be32(out, HEADER_FIELD_END);
for (Map::const_iterator it(entries.begin()); it != entries.end(); ++it) { for (Map::const_iterator it(entries.begin()); it != entries.end(); ++it) {
it->second.store(out); it->second.store(out);

View File

@@ -83,18 +83,23 @@ public:
void set_key_name (const char* k) { key_name = k ? k : ""; } 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(); } 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: private:
typedef std::map<uint32_t, Entry, std::greater<uint32_t> > Map; typedef std::map<uint32_t, Entry, std::greater<uint32_t> > Map;
enum { FORMAT_VERSION = 2 }; enum { FORMAT_VERSION = 2 };
Map entries; Map entries;
std::string key_name; std::string key_name;
bool skip_empty = false;
void load_header (std::istream&); void load_header (std::istream&);
enum { enum {
HEADER_FIELD_END = 0, 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 { enum {
KEY_FIELD_END = 0, KEY_FIELD_END = 0,

View File

@@ -7,8 +7,8 @@
--> -->
<refentryinfo> <refentryinfo>
<title>git-crypt</title> <title>git-crypt</title>
<date>2022-04-21</date> <date>2017-11-26</date>
<productname>git-crypt 0.7.0</productname> <productname>git-crypt 0.6.0</productname>
<author> <author>
<othername>Andrew Ayer</othername> <othername>Andrew Ayer</othername>