diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e4504cf --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.git* export-ignore diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8b4b482 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,25 @@ +Thanks for your interest in contributing to git-crypt! git-crypt is +open source software and welcomes contributions in the form of code, +documentation, bug reports, or anything else that improves git-crypt. + +When contributing code, please consider the following guidelines: + + * 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. + + * Please mimic the existing code style as much as possible. In + particular, please indent code using tab characters with a width + of 8. + + * To minimize merge commits, please rebase your changes before opening + a pull request. + + * 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, +the bar for contributions is higher than average. Please don't be +discouraged by this, but be prepared for patches to possibly go through +several rounds of feedback and improvement before being accepted. +Your patience and understanding is appreciated. diff --git a/INSTALL b/INSTALL index 0bd4da8..9c8e8e0 100644 --- a/INSTALL +++ b/INSTALL @@ -2,10 +2,8 @@ DEPENDENCIES To use git-crypt, you need: - * Git 1.6.0 or newer + * Git 1.7.2 or newer * OpenSSL - * For decrypted git diff output, Git 1.6.1 or newer - * For decrypted git blame output, Git 1.7.2 or newer To build git-crypt, you need a C++ compiler and OpenSSL development headers. @@ -20,3 +18,30 @@ The Makefile is tailored for g++, but should work with other compilers. It doesn't matter where you install the git-crypt binary - choose wherever is most convenient for you. + + +BUILDING A DEBIAN PACKAGE + +Debian packaging can be found in the 'debian' branch of the project +Git repository. The package is built using git-buildpackage as follows: + + $ git checkout debian + $ git-buildpackage -uc -us + + +INSTALLING ON MAC OS X + +Using the brew package manager, simply run: + + $ brew install git-crypt + + +EXPERIMENTAL WINDOWS SUPPORT + +git-crypt should build on Windows with MinGW, although the build system +is not yet finalized so you will need to pass your own CXX, CXXFLAGS, and +LDFLAGS variables to make. Additionally, Windows support is less tested +and does not currently create key files with restrictive permissions, +making it unsuitable for use on a multi-user system. Windows support +will mature in a future version of git-crypt. Bug reports and patches +are most welcome! diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..b4ee879 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,51 @@ +Dependencies +------------ + +To use git-crypt, you need: + +* Git 1.7.2 or newer +* OpenSSL + +To build git-crypt, you need a C++ compiler and OpenSSL development +headers. + + +Building git-crypt +------------------ + +The Makefile is tailored for g++, but should work with other compilers. + + make + cp git-crypt /usr/local/bin/ + +It doesn't matter where you install the git-crypt binary - choose +wherever is most convenient for you. + + +Building A Debian Package +------------------------- + +Debian packaging can be found in the 'debian' branch of the project Git +repository. The package is built using git-buildpackage as follows: + + git checkout debian + git-buildpackage -uc -us + + +Installing On Mac OS X +---------------------- + +Using the brew package manager, simply run: + + brew install git-crypt + +Experimental Windows Support +---------------------------- + +git-crypt should build on Windows with MinGW, although the build system +is not yet finalized so you will need to pass your own CXX, CXXFLAGS, and +LDFLAGS variables to make. Additionally, Windows support is less tested +and does not currently create key files with restrictive permissions, +making it unsuitable for use on a multi-user system. Windows support +will mature in a future version of git-crypt. Bug reports and patches +are most welcome! diff --git a/Makefile b/Makefile index f0b8e60..463b502 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,31 @@ CXX := c++ -CXXFLAGS := -Wall -pedantic -ansi -Wno-long-long -O2 -LDFLAGS := -lcrypto +CXXFLAGS := -Wall -pedantic -Wno-long-long -O2 +LDFLAGS := PREFIX := /usr/local -OBJFILES = git-crypt.o commands.o crypto.o util.o +OBJFILES = \ + git-crypt.o \ + commands.o \ + crypto.o \ + gpg.o \ + key.o \ + util.o \ + parse_options.o + +OBJFILES += crypto-openssl.o +LDFLAGS += -lcrypto all: git-crypt git-crypt: $(OBJFILES) - $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) + $(CXX) $(CXXFLAGS) -o $@ $(OBJFILES) $(LDFLAGS) + +util.o: util.cpp util-unix.cpp util-win32.cpp clean: rm -f *.o git-crypt -install: +install: git-crypt install -m 755 git-crypt $(DESTDIR)$(PREFIX)/bin/ .PHONY: all clean install diff --git a/NEWS b/NEWS index 9a4081e..f51ea1f 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,31 @@ +v0.4.2 (2015-01-31) + * Fix unlock and lock under Git 2.2.2 and higher. + * Drop support for versions of Git older than 1.7.2. + * Minor improvements to some help/error messages. + +v0.4.1 (2015-01-08) + * Important usability fix to ensure that the .git-crypt directory + can't be encrypted by accident (see RELEASE_NOTES-0.4.1.md for + more information). + +v0.4 (2014-11-16) + (See RELEASE_NOTES-0.4.md for important details.) + * Add optional GPG support: GPG can be used to share the repository + between one or more users in lieu of sharing a secret key. + * New workflow: the symmetric key is now stored inside the .git + directory. Although backwards compatibility has been preserved + with repositories created by old versions of git-crypt, the + commands for setting up a repository have changed. See the + release notes file for details. + * Multiple key support: it's now possible to encrypt different parts + of a repository with different keys. + * Initial 'git-crypt status' command to report which files are + encrypted and to fix problems that are detected. + * Numerous usability, documentation, and error reporting improvements. + * Major internal code improvements that will make future development + easier. + * Initial experimental Windows support. + v0.3 (2013-04-05) * Fix 'git-crypt init' on newer versions of Git. Previously, encrypted files were not being automatically decrypted after diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..5da8f6b --- /dev/null +++ b/NEWS.md @@ -0,0 +1,49 @@ +News +==== + +######v0.4.2 (2015-01-31) +* Fix unlock and lock under Git 2.2.2 and higher. +* Drop support for versions of Git older than 1.7.2. +* Minor improvements to some help/error messages. + +######v0.4.1 (2015-01-08) +* Important usability fix to ensure that the .git-crypt directory + can't be encrypted by accident (see + [the release notes](RELEASE_NOTES-0.4.1.md) for more information). + +######v0.4 (2014-11-16) +(See [the release notes](RELEASE_NOTES-0.4.md) for important details.) +* Add optional GPG support: GPG can be used to share the repository + between one or more users in lieu of sharing a secret key. +* New workflow: the symmetric key is now stored inside the .git + directory. Although backwards compatibility has been preserved + with repositories created by old versions of git-crypt, the + commands for setting up a repository have changed. See the + release notes file for details. +* Multiple key support: it's now possible to encrypt different parts + of a repository with different keys. +* Initial `git-crypt status` command to report which files are + encrypted and to fix problems that are detected. +* Numerous usability, documentation, and error reporting improvements. +* Major internal code improvements that will make future development + easier. +* Initial experimental Windows support. + +######v0.3 (2013-04-05) +* Fix `git-crypt init` on newer versions of Git. Previously, + encrypted files were not being automatically decrypted after running + `git-crypt init` with recent versions of Git. +* Allow `git-crypt init` to be run even if the working tree contains + untracked files. +* `git-crypt init` now properly escapes arguments to the filter + commands it configures, allowing both the path to git-crypt and the + path to the key file to contain arbitrary characters such as spaces. + +######v0.2 (2013-01-25) +* Numerous improvements to `git-crypt init` usability. +* Fix gitattributes example in [README](README.md): the old example + showed a colon after the filename where there shouldn't be one. +* Various build fixes and improvements. + +######v0.1 (2012-11-29) +* Initial release. diff --git a/README b/README index d31ae8c..2d90b29 100644 --- a/README +++ b/README @@ -10,8 +10,8 @@ you store your secret material (such as keys or passwords) in the same repository as your code, without requiring you to lock down your entire repository. -git-crypt was written by Andrew Ayer . -For more information, see . +git-crypt was written by Andrew Ayer . For more +information, see . BUILDING GIT-CRYPT @@ -21,48 +21,58 @@ See the INSTALL file. USING GIT-CRYPT -Generate a secret key: - - $ git-crypt keygen /path/to/keyfile - -Configure a repository to use encryption: +Configure a repository to use git-crypt: $ cd repo - $ git-crypt init /path/to/keyfile + $ git-crypt init Specify files to encrypt by creating a .gitattributes file: secretfile filter=git-crypt diff=git-crypt *.key filter=git-crypt diff=git-crypt -Like a .gitignore file, it can match wildcards and should be checked -into the repository. Make sure you don't accidentally encrypt the -.gitattributes file itself! +Like a .gitignore file, it can match wildcards and should be checked into +the repository. See below for more information about .gitattributes. +Make sure you don't accidentally encrypt the .gitattributes file itself! -Cloning a repository with encrypted files: +Share the repository with others (or with yourself) using GPG: - $ git clone /path/to/repo - $ cd repo - $ git-crypt init /path/to/keyfile + $ git-crypt add-gpg-user USER_ID -That's all you need to do - after running git-crypt init, you can use -git normally - encryption and decryption happen transparently. +USER_ID can be a key ID, a full fingerprint, an email address, or anything +else that uniquely identifies a public key to GPG (see "HOW TO SPECIFY +A USER ID" in the gpg man page). Note: `git-crypt add-gpg-user` will +add and commit a GPG-encrypted key file in the .git-crypt directory of +the root of your repository. + +Alternatively, you can export a symmetric secret key, which you must +securely convey to collaborators (GPG is not required, and no files +are added to your repository): + + $ git-crypt export-key /path/to/key + +After cloning a repository with encrypted files, unlock with with GPG: + + $ git-crypt unlock + +Or with a symmetric key: + + $ git-crypt unlock /path/to/key + +That's all you need to do - after git-crypt is set up (either with +`git-crypt init` or `git-crypt unlock`), you can use git normally - +encryption and decryption happen transparently. CURRENT STATUS -The latest version of git-crypt is 0.3, released on 2013-04-05. +The latest version of git-crypt is 0.4.2, released on 2015-01-31. git-crypt aims to be bug-free and reliable, meaning it shouldn't crash, malfunction, or expose your confidential data. However, it has not yet reached maturity, meaning it is not as documented, featureful, or easy-to-use as it should be. Additionally, there may be backwards-incompatible changes introduced before version 1.0. -Development on git-crypt is currently focused on improving the user -experience, especially around setting up repositories. There are also -plans to add additional key management schemes, such as passphrase-derived -keys and keys encrypted with PGP. - SECURITY @@ -76,27 +86,64 @@ beyond whether two files are identical or not. Other proposals for transparent git encryption use ECB or CBC with a fixed IV. These systems are not semantically secure and leak information. -The AES key is stored unencrypted on disk. The user is responsible for -protecting it and ensuring it's safely distributed only to authorized -people. A future version of git-crypt may support encrypting the key -with a passphrase. - LIMITATIONS -git-crypt is not designed to encrypt an entire repository. Not only does -that defeat the aim of git-crypt, which is the ability to selectively -encrypt files and share the repository with less-trusted developers, there -are probably better, more efficient ways to encrypt an entire repository, -such as by storing it on an encrypted filesystem. Also note that -git-crypt is somewhat of an abuse of git's smudge, clean, and textconv -features. Junio Hamano, git's maintainer, has said not to do this -, -though his main objection ("making a pair of similar 'smudged' contents -totally dissimilar in their 'clean' counterparts.") does not apply here -since git-crypt uses deterministic encryption. +git-crypt relies on git filters, which were not designed with encryption +in mind. As such, git-crypt is not the best tool for encrypting most or +all of the files in a repository. Where git-crypt really shines is where +most of your repository is public, but you have a few files (perhaps +private keys named *.key, or a file with API credentials) which you +need to encrypt. For encrypting an entire repository, consider using a +system like git-remote-gcrypt +instead. (Note: no endorsement is made of git-remote-gcrypt's security.) -git-crypt does not itself provide any authentication. It assumes that -either the master copy of your repository is stored securely, or that -you are using git's existing facilities to ensure integrity (signed tags, -remembering commit hashes, etc.). +git-crypt does not encrypt file names, commit messages, or other metadata. + +Files encrypted with git-crypt are not compressible. Even the smallest +change to an encrypted file requires git to store the entire changed file, +instead of just a delta. + +Although git-crypt protects individual file contents with a SHA-1 +HMAC, git-crypt cannot be used securely unless the entire repository is +protected against tampering (an attacker who can mutate your repository +can alter your .gitattributes file to disable encryption). If necessary, +use git features such as signed tags instead of relying solely on +git-crypt for integrity. + +Files encrypted with git-crypt cannot be patched with git-apply, unless +the patch itself is encrypted. To generate an encrypted patch, use `git +diff --no-textconv --binary`. Alternatively, you can apply a plaintext +patch outside of git using the patch command. + +git-crypt does not work reliably with Atlassian SourceTree. +Files might be left in an unencrypted state. See +. + + +GITATTRIBUTES FILE + +The .gitattributes file is documented in the gitattributes(5) man page. +The file pattern format is the same as the one used by .gitignore, +as documented in the gitignore(5) man page, with the exception that +specifying merely a directory (e.g. `/dir/`) is NOT sufficient to +encrypt all files beneath it. + +Also note that the pattern `dir/*` does not match files under +sub-directories of dir/. To encrypt an entire sub-tree dir/, place the +following in dir/.gitattributes: + + * filter=git-crypt diff=git-crypt + .gitattributes !filter !diff + +The second pattern is essential for ensuring that .gitattributes itself +is not encrypted. + + +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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..c5759c4 --- /dev/null +++ b/README.md @@ -0,0 +1,151 @@ +git-crypt - transparent file encryption in git +============================================== + +git-crypt enables transparent encryption and decryption of files in a +git repository. Files which you choose to protect are encrypted when +committed, and decrypted when checked out. git-crypt lets you freely +share a repository containing a mix of public and private content. +git-crypt gracefully degrades, so developers without the secret key can +still clone and commit to a repository with encrypted files. This lets +you store your secret material (such as keys or passwords) in the same +repository as your code, without requiring you to lock down your entire +repository. + +git-crypt was written by [Andrew Ayer](https://www.agwa.name) (agwa@andrewayer.name). +For more information, see . + +Building git-crypt +------------------ +See the [INSTALL.md](INSTALL.md) file. + + +Using git-crypt +--------------- + +Configure a repository to use git-crypt: + + cd repo + git-crypt init + +Specify files to encrypt by creating a .gitattributes file: + + secretfile filter=git-crypt diff=git-crypt + *.key filter=git-crypt diff=git-crypt + +Like a .gitignore file, it can match wildcards and should be checked into +the repository. See below for more information about .gitattributes. +Make sure you don't accidentally encrypt the .gitattributes file itself! + +Share the repository with others (or with yourself) using GPG: + + git-crypt add-gpg-user USER_ID + +`USER_ID` can be a key ID, a full fingerprint, an email address, or +anything else that uniquely identifies a public key to GPG (see "HOW TO +SPECIFY A USER ID" in the gpg man page). Note: `git-crypt add-gpg-user` +will add and commit a GPG-encrypted key file in the .git-crypt directory +of the root of your repository. + +Alternatively, you can export a symmetric secret key, which you must +securely convey to collaborators (GPG is not required, and no files +are added to your repository): + + git-crypt export-key /path/to/key + +After cloning a repository with encrypted files, unlock with with GPG: + + git-crypt unlock + +Or with a symmetric key: + + git-crypt unlock /path/to/key + +That's all you need to do - after git-crypt is set up (either with +`git-crypt init` or `git-crypt unlock`), you can use git normally - +encryption and decryption happen transparently. + +Current Status +-------------- + +The latest version of git-crypt is [0.4.2](NEWS.md), released on +2015-01-31. git-crypt aims to be bug-free and reliable, meaning it +shouldn't crash, malfunction, or expose your confidential data. +However, it has not yet reached maturity, meaning it is not as +documented, featureful, or easy-to-use as it should be. Additionally, +there may be backwards-incompatible changes introduced before version +1.0. + +Security +-------- + +git-crypt is more secure that other transparent git encryption systems. +git-crypt encrypts files using AES-256 in CTR mode with a synthetic IV +derived from the SHA-1 HMAC of the file. This is provably semantically +secure under deterministic chosen-plaintext attack. That means that +although the encryption is deterministic (which is required so git can +distinguish when a file has and hasn't changed), it leaks no information +beyond whether two files are identical or not. Other proposals for +transparent git encryption use ECB or CBC with a fixed IV. These +systems are not semantically secure and leak information. + +Limitations +----------- + +git-crypt relies on git filters, which were not designed with encryption +in mind. As such, git-crypt is not the best tool for encrypting most or +all of the files in a repository. Where git-crypt really shines is where +most of your repository is public, but you have a few files (perhaps +private keys named *.key, or a file with API credentials) which you +need to encrypt. For encrypting an entire repository, consider using a +system like [git-remote-gcrypt](https://github.com/joeyh/git-remote-gcrypt) +instead. (Note: no endorsement is made of git-remote-gcrypt's security.) + +git-crypt does not encrypt file names, commit messages, or other metadata. + +Files encrypted with git-crypt are not compressible. Even the smallest +change to an encrypted file requires git to store the entire changed file, +instead of just a delta. + +Although git-crypt protects individual file contents with a SHA-1 +HMAC, git-crypt cannot be used securely unless the entire repository is +protected against tampering (an attacker who can mutate your repository +can alter your .gitattributes file to disable encryption). If necessary, +use git features such as signed tags instead of relying solely on +git-crypt for integrity. + +Files encrypted with git-crypt cannot be patched with git-apply, unless +the patch itself is encrypted. To generate an encrypted patch, use `git +diff --no-textconv --binary`. Alternatively, you can apply a plaintext +patch outside of git using the patch command. + +git-crypt does [not work reliably with Atlassian +SourceTree](https://jira.atlassian.com/browse/SRCTREE-2511). Files might +be left in an unencrypted state. + +Gitattributes File +------------------ + +The .gitattributes file is documented in the gitattributes(5) man page. +The file pattern format is the same as the one used by .gitignore, +as documented in the gitignore(5) man page, with the exception that +specifying merely a directory (e.g. `/dir/`) is *not* sufficient to +encrypt all files beneath it. + +Also note that the pattern `dir/*` does not match files under +sub-directories of dir/. To encrypt an entire sub-tree dir/, place the +following in dir/.gitattributes: + + * filter=git-crypt diff=git-crypt + .gitattributes !filter !diff + +The second pattern is essential for ensuring that .gitattributes itself +is not encrypted. + +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) diff --git a/RELEASE_NOTES-0.4.1.md b/RELEASE_NOTES-0.4.1.md new file mode 100644 index 0000000..ec64dd6 --- /dev/null +++ b/RELEASE_NOTES-0.4.1.md @@ -0,0 +1,21 @@ +git-crypt 0.4.1 is a bugfix-only release that contains an important +usability fix for users who use GPG mode to encrypt an entire repository. + +Previously, if you used a '*' pattern in the top-level .gitattributes +file, and you did not explicitly add a pattern to exclude the .git-crypt +directory, the files contained therein would be encrypted, rendering +the repository impossible to unlock with GPG. + +git-crypt now adds a .gitattributes file to the .git-crypt directory +to prevent its contents from being encrypted, regardless of patterns in +the top-level .gitattributes. + +If you are using git-crypt in GPG mode to encrypt an entire repository, +and you do not already have a .gitattributes pattern to exclude the +.git-crypt directory, you are strongly advised to upgrade. After +upgrading, you should do the following in each of your repositories to +ensure that the information inside .git-crypt is properly stored: + +1. Remove existing key files: `rm .git-crypt/keys/*/0/*` + +2. Re-add GPG user(s): `git-crypt add-gpg-user GPG_USER_ID ...` diff --git a/RELEASE_NOTES-0.4.md b/RELEASE_NOTES-0.4.md new file mode 100644 index 0000000..41e2634 --- /dev/null +++ b/RELEASE_NOTES-0.4.md @@ -0,0 +1,84 @@ +Changes to be aware of for git-crypt 0.4 +======================================== + +(For a complete list of changes, see the [NEWS](NEWS.md) file.) + + +### New workflow + +The commands for setting up a repository have changed in git-crypt 0.4. +The previous commands continue to work, but will be removed in a future +release of git-crypt. Please get in the habit of using the new syntax: + +`git-crypt init` no longer takes an argument, and is now used only when +initializing a repository for the very first time. It generates a key +and stores it in the `.git` directory. There is no longer a separate +`keygen` step, and you no longer need to keep a copy of the key outside +the repository. + +`git-crypt init` is no longer used to decrypt a cloned repository. Instead, +run `git-crypt unlock /path/to/keyfile`, where `keyfile` is obtained by +running `git-crypt export-key /path/to/keyfile` from an already-decrypted +repository. + + +### GPG mode + +git-crypt now supports GPG. A repository can be shared with one or more +GPG users in lieu of sharing a secret symmetric key. Symmetric key support +isn't going away, but the workflow of GPG mode is extremely easy and all users +are encouraged to consider it for their repositories. + +See the [README](README.md) for details on using GPG. + + +### Status command + +A new command, `git-crypt status`, lists encrypted files, which is +useful for making sure your `.gitattributes` pattern is protecting the +right files. + + +### Multiple key support + +git-crypt now lets you encrypt different sets of files with different +keys, which is useful if you want to grant different collaborators access +to different sets of files. + +See [doc/multiple_keys.md](doc/multiple_keys.md) for details. + + +### Compatibility with old repositories + +Repositories created with older versions of git-crypt continue to work +without any changes needed, and backwards compatibility with these +repositories will be maintained indefinitely. + +However, you will not be able to take advantage of git-crypt's new +features, such as GPG support, unless you migrate your repository. + +To migrate your repository, first ensure the working tree is clean. +Then migrate your current key file and use the migrated key to unlock +your repository as follows: + + git-crypt migrate-key /path/to/old_key /path/to/migrated_key + git-crypt unlock /path/to/migrated_key + +Once you've confirmed that your repository is functional, you can delete +both the old and migrated key files (though keeping a backup of your key +is always a good idea). + + +### Known issues + +It is not yet possible to revoke access from a GPG user. This will +require substantial development work and will be a major focus of future +git-crypt development. + +The output of `git-crypt status` is currently very bare-bones and will +be substantially improved in a future release. Do not rely on its output +being stable. A future release of git-crypt will provide an option for stable +machine-readable output. + +On Windows, git-crypt does not create key files with restrictive +permissions. Take care when using git-crypt on a multi-user Windows system. diff --git a/THANKS.md b/THANKS.md new file mode 100644 index 0000000..4c0d53e --- /dev/null +++ b/THANKS.md @@ -0,0 +1,19 @@ +For their contributions to git-crypt, I thank: + + * Michael Mior and @zimbatm for the Homebrew formula. + + * Cyril Cleaud for help with Windows support. + + * The following people for contributing patches: + * Adam Nelson + * Caleb Maclennan + * Darayus Nanavati + * Jon Sailor + * Linus G Thiel + * Michael Schout + * Simon Kotlinski + + * And everyone who has tested git-crypt, provided feedback, reported + bugs, and participated in discussions about new features. + +Thank you! diff --git a/commands.cpp b/commands.cpp index b3180c5..ef40b21 100644 --- a/commands.cpp +++ b/commands.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -31,8 +31,9 @@ #include "commands.hpp" #include "crypto.hpp" #include "util.hpp" -#include -#include +#include "key.hpp" +#include "gpg.hpp" +#include "parse_options.hpp" #include #include #include @@ -42,29 +43,498 @@ #include #include #include -#include -#include +#include +#include +#include +#include +#include + +static std::string attribute_name (const char* key_name) +{ + if (key_name) { + // named key + return std::string("git-crypt-") + key_name; + } else { + // default key + return "git-crypt"; + } +} + +static void git_config (const std::string& name, const std::string& value) +{ + std::vector command; + command.push_back("git"); + command.push_back("config"); + command.push_back(name); + command.push_back(value); + + if (!successful_exit(exec_command(command))) { + throw Error("'git config' failed"); + } +} + +static void git_unconfig (const std::string& name) +{ + std::vector command; + command.push_back("git"); + command.push_back("config"); + command.push_back("--remove-section"); + command.push_back(name); + + if (!successful_exit(exec_command(command))) { + throw Error("'git config' failed"); + } +} + +static void configure_git_filters (const char* key_name) +{ + std::string escaped_git_crypt_path(escape_shell_arg(our_exe_path())); + + if (key_name) { + // Note: key_name contains only shell-safe characters so it need not be escaped. + git_config(std::string("filter.git-crypt-") + key_name + ".smudge", + escaped_git_crypt_path + " smudge --key-name=" + key_name); + git_config(std::string("filter.git-crypt-") + key_name + ".clean", + escaped_git_crypt_path + " clean --key-name=" + key_name); + git_config(std::string("filter.git-crypt-") + key_name + ".required", "true"); + git_config(std::string("diff.git-crypt-") + key_name + ".textconv", + escaped_git_crypt_path + " diff --key-name=" + key_name); + } else { + git_config("filter.git-crypt.smudge", escaped_git_crypt_path + " smudge"); + git_config("filter.git-crypt.clean", escaped_git_crypt_path + " clean"); + git_config("filter.git-crypt.required", "true"); + git_config("diff.git-crypt.textconv", escaped_git_crypt_path + " diff"); + } +} + +static void unconfigure_git_filters (const char* key_name) +{ + // unconfigure the git-crypt filters + git_unconfig("filter." + attribute_name(key_name)); + git_unconfig("diff." + attribute_name(key_name)); +} + +static bool git_checkout (const std::vector& paths) +{ + std::vector command; + + command.push_back("git"); + command.push_back("checkout"); + command.push_back("--"); + + for (std::vector::const_iterator path(paths.begin()); path != paths.end(); ++path) { + command.push_back(*path); + } + + if (!successful_exit(exec_command(command))) { + return false; + } + + return true; +} + +static bool same_key_name (const char* a, const char* b) +{ + return (!a && !b) || (a && b && std::strcmp(a, b) == 0); +} + +static void validate_key_name_or_throw (const char* key_name) +{ + std::string reason; + if (!validate_key_name(key_name, &reason)) { + throw Error(reason); + } +} + +static std::string get_internal_state_path () +{ + // git rev-parse --git-dir + std::vector command; + command.push_back("git"); + command.push_back("rev-parse"); + command.push_back("--git-dir"); + + std::stringstream output; + + if (!successful_exit(exec_command(command, output))) { + throw Error("'git rev-parse --git-dir' failed - is this a Git repository?"); + } + + std::string path; + std::getline(output, path); + path += "/git-crypt"; + + return path; +} + +static std::string get_internal_keys_path (const std::string& internal_state_path) +{ + return internal_state_path + "/keys"; +} + +static std::string get_internal_keys_path () +{ + return get_internal_keys_path(get_internal_state_path()); +} + +static std::string get_internal_key_path (const char* key_name) +{ + std::string path(get_internal_keys_path()); + path += "/"; + path += key_name ? key_name : "default"; + + return path; +} + +static std::string get_repo_state_path () +{ + // git rev-parse --show-toplevel + std::vector command; + command.push_back("git"); + command.push_back("rev-parse"); + command.push_back("--show-toplevel"); + + std::stringstream output; + + if (!successful_exit(exec_command(command, output))) { + throw Error("'git rev-parse --show-toplevel' failed - is this a Git repository?"); + } + + std::string path; + std::getline(output, path); + + if (path.empty()) { + // could happen for a bare repo + throw Error("Could not determine Git working tree - is this a non-bare repo?"); + } + + path += "/.git-crypt"; + return path; +} + +static std::string get_repo_keys_path (const std::string& repo_state_path) +{ + return repo_state_path + "/keys"; +} + +static std::string get_repo_keys_path () +{ + return get_repo_keys_path(get_repo_state_path()); +} + +static std::string get_path_to_top () +{ + // git rev-parse --show-cdup + std::vector command; + command.push_back("git"); + command.push_back("rev-parse"); + command.push_back("--show-cdup"); + + std::stringstream output; + + if (!successful_exit(exec_command(command, output))) { + throw Error("'git rev-parse --show-cdup' failed - is this a Git repository?"); + } + + std::string path_to_top; + std::getline(output, path_to_top); + + return path_to_top; +} + +static void get_git_status (std::ostream& output) +{ + // git status -uno --porcelain + std::vector command; + command.push_back("git"); + command.push_back("status"); + command.push_back("-uno"); // don't show untracked files + command.push_back("--porcelain"); + + if (!successful_exit(exec_command(command, output))) { + throw Error("'git status' failed - is this a Git repository?"); + } +} + +// returns filter and diff attributes as a pair +static std::pair get_file_attributes (const std::string& filename) +{ + // git check-attr filter diff -- filename + // TODO: pass -z to get machine-parseable output (this requires Git 1.8.5 or higher, which was released on 27 Nov 2013) + std::vector command; + command.push_back("git"); + command.push_back("check-attr"); + command.push_back("filter"); + command.push_back("diff"); + command.push_back("--"); + command.push_back(filename); + + std::stringstream output; + if (!successful_exit(exec_command(command, output))) { + throw Error("'git check-attr' failed - is this a Git repository?"); + } + + std::string filter_attr; + std::string diff_attr; + + std::string line; + // Example output: + // filename: filter: git-crypt + // filename: diff: git-crypt + while (std::getline(output, line)) { + // filename might contain ": ", so parse line backwards + // filename: attr_name: attr_value + // ^name_pos ^value_pos + const std::string::size_type value_pos(line.rfind(": ")); + if (value_pos == std::string::npos || value_pos == 0) { + continue; + } + const std::string::size_type name_pos(line.rfind(": ", value_pos - 1)); + if (name_pos == std::string::npos) { + continue; + } + + const std::string attr_name(line.substr(name_pos + 2, value_pos - (name_pos + 2))); + const std::string attr_value(line.substr(value_pos + 2)); + + if (attr_value != "unspecified" && attr_value != "unset" && attr_value != "set") { + if (attr_name == "filter") { + filter_attr = attr_value; + } else if (attr_name == "diff") { + diff_attr = attr_value; + } + } + } + + return std::make_pair(filter_attr, diff_attr); +} + +static bool check_if_blob_is_encrypted (const std::string& object_id) +{ + // git cat-file blob object_id + + std::vector 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?"); + } + + char header[10]; + output.read(header, sizeof(header)); + return output.gcount() == sizeof(header) && std::memcmp(header, "\0GITCRYPT\0", 10) == 0; +} + +static bool check_if_file_is_encrypted (const std::string& filename) +{ + // git ls-files -sz filename + std::vector command; + command.push_back("git"); + command.push_back("ls-files"); + command.push_back("-sz"); + command.push_back("--"); + command.push_back(filename); + + std::stringstream output; + if (!successful_exit(exec_command(command, output))) { + throw Error("'git ls-files' failed - is this a Git repository?"); + } + + if (output.peek() == -1) { + return false; + } + + std::string mode; + std::string object_id; + output >> mode >> object_id; + + return check_if_blob_is_encrypted(object_id); +} + +static void get_encrypted_files (std::vector& files, const char* key_name) +{ + // git ls-files -cz -- path_to_top + std::vector command; + command.push_back("git"); + command.push_back("ls-files"); + command.push_back("-cz"); + command.push_back("--"); + const std::string path_to_top(get_path_to_top()); + if (!path_to_top.empty()) { + command.push_back(path_to_top); + } + + std::stringstream output; + if (!successful_exit(exec_command(command, output))) { + throw Error("'git ls-files' failed - is this a Git repository?"); + } + + while (output.peek() != -1) { + std::string filename; + std::getline(output, filename, '\0'); + + // TODO: get file attributes en masse for efficiency... unfortunately this requires machine-parseable output from git check-attr to be workable, and this is only supported in Git 1.8.5 and above (released 27 Nov 2013) + if (get_file_attributes(filename).first == attribute_name(key_name)) { + files.push_back(filename); + } + } +} + +static void load_key (Key_file& key_file, const char* key_name, const char* key_path =0, const char* legacy_path =0) +{ + if (legacy_path) { + std::ifstream key_file_in(legacy_path, std::fstream::binary); + if (!key_file_in) { + throw Error(std::string("Unable to open key file: ") + legacy_path); + } + key_file.load_legacy(key_file_in); + } else if (key_path) { + std::ifstream key_file_in(key_path, std::fstream::binary); + if (!key_file_in) { + throw Error(std::string("Unable to open key file: ") + key_path); + } + key_file.load(key_file_in); + } else { + std::ifstream key_file_in(get_internal_key_path(key_name).c_str(), std::fstream::binary); + if (!key_file_in) { + // TODO: include key name in error message + throw Error("Unable to open key file - have you unlocked/initialized this repository yet?"); + } + key_file.load(key_file_in); + } +} + +static bool decrypt_repo_key (Key_file& key_file, const char* key_name, uint32_t key_version, const std::vector& secret_keys, const std::string& keys_path) +{ + for (std::vector::const_iterator seckey(secret_keys.begin()); seckey != secret_keys.end(); ++seckey) { + std::ostringstream path_builder; + path_builder << keys_path << '/' << (key_name ? key_name : "default") << '/' << key_version << '/' << *seckey << ".gpg"; + std::string path(path_builder.str()); + if (access(path.c_str(), F_OK) == 0) { + std::stringstream decrypted_contents; + gpg_decrypt_from_file(path, decrypted_contents); + Key_file this_version_key_file; + this_version_key_file.load(decrypted_contents); + const Key_file::Entry* this_version_entry = this_version_key_file.get(key_version); + if (!this_version_entry) { + throw Error("GPG-encrypted keyfile is malformed because it does not contain expected key version"); + } + if (!same_key_name(key_name, this_version_key_file.get_key_name())) { + throw Error("GPG-encrypted keyfile is malformed because it does not contain expected key name"); + } + key_file.set_key_name(key_name); + key_file.add(*this_version_entry); + return true; + } + } + return false; +} + +static bool decrypt_repo_keys (std::vector& key_files, uint32_t key_version, const std::vector& secret_keys, const std::string& keys_path) +{ + bool successful = false; + std::vector dirents; + + if (access(keys_path.c_str(), F_OK) == 0) { + dirents = get_directory_contents(keys_path.c_str()); + } + + for (std::vector::const_iterator dirent(dirents.begin()); dirent != dirents.end(); ++dirent) { + const char* key_name = 0; + if (*dirent != "default") { + if (!validate_key_name(dirent->c_str())) { + continue; + } + key_name = dirent->c_str(); + } + + Key_file key_file; + if (decrypt_repo_key(key_file, key_name, key_version, secret_keys, keys_path)) { + key_files.push_back(key_file); + successful = true; + } + } + return successful; +} + +static void encrypt_repo_key (const char* key_name, const Key_file::Entry& key, const std::vector& collab_keys, const std::string& keys_path, std::vector* new_files) +{ + std::string key_file_data; + { + Key_file this_version_key_file; + this_version_key_file.set_key_name(key_name); + this_version_key_file.add(key); + key_file_data = this_version_key_file.store_to_string(); + } + + for (std::vector::const_iterator collab(collab_keys.begin()); collab != collab_keys.end(); ++collab) { + std::ostringstream path_builder; + path_builder << keys_path << '/' << (key_name ? key_name : "default") << '/' << key.version << '/' << *collab << ".gpg"; + std::string path(path_builder.str()); + + if (access(path.c_str(), F_OK) == 0) { + continue; + } + + mkdir_parent(path); + gpg_encrypt_to_file(path, *collab, key_file_data.data(), key_file_data.size()); + new_files->push_back(path); + } +} + +static int parse_plumbing_options (const char** key_name, const char** key_file, int argc, const char** argv) +{ + Options_list options; + options.push_back(Option_def("-k", key_name)); + options.push_back(Option_def("--key-name", key_name)); + options.push_back(Option_def("--key-file", key_file)); + + return parse_options(options, argc, argv); +} // Encrypt contents of stdin and write to stdout -void clean (const char* keyfile) +int clean (int argc, const char** argv) { - keys_t keys; - load_keys(keyfile, &keys); + const char* key_name = 0; + const char* key_path = 0; + const char* legacy_key_path = 0; + + int argi = parse_plumbing_options(&key_name, &key_path, argc, argv); + if (argc - argi == 0) { + } else if (!key_name && !key_path && argc - argi == 1) { // Deprecated - for compatibility with pre-0.4 + legacy_key_path = argv[argi]; + } else { + std::clog << "Usage: git-crypt clean [--key-name=NAME] [--key-file=PATH]" << std::endl; + return 2; + } + Key_file key_file; + load_key(key_file, key_name, key_path, legacy_key_path); + + const Key_file::Entry* key = key_file.get_latest(); + if (!key) { + std::clog << "git-crypt: error: key file is empty" << std::endl; + return 1; + } // Read the entire file - hmac_sha1_state hmac(keys.hmac, HMAC_KEY_LEN); // Calculate the file's SHA1 HMAC as we go - uint64_t file_size = 0; // Keep track of the length, make sure it doesn't get too big - std::string file_contents; // First 8MB or so of the file go here - std::fstream temp_file; // The rest of the file spills into a temporary file on disk + Hmac_sha1_state hmac(key->hmac_key, HMAC_KEY_LEN); // Calculate the file's SHA1 HMAC as we go + uint64_t file_size = 0; // Keep track of the length, make sure it doesn't get too big + std::string file_contents; // First 8MB or so of the file go here + temp_fstream temp_file; // The rest of the file spills into a temporary file on disk temp_file.exceptions(std::fstream::badbit); - char buffer[1024]; + char buffer[1024]; - while (std::cin && file_size < MAX_CRYPT_BYTES) { + while (std::cin && file_size < Aes_ctr_encryptor::MAX_CRYPT_BYTES) { std::cin.read(buffer, sizeof(buffer)); - size_t bytes_read = std::cin.gcount(); + const size_t bytes_read = std::cin.gcount(); hmac.add(reinterpret_cast(buffer), bytes_read); file_size += bytes_read; @@ -73,19 +543,18 @@ void clean (const char* keyfile) file_contents.append(buffer, bytes_read); } else { if (!temp_file.is_open()) { - open_tempfile(temp_file, std::fstream::in | std::fstream::out | std::fstream::binary | std::fstream::app); + temp_file.open(std::fstream::in | std::fstream::out | std::fstream::binary | std::fstream::app); } temp_file.write(buffer, bytes_read); } } // Make sure the file isn't so large we'll overflow the counter value (which would doom security) - if (file_size >= MAX_CRYPT_BYTES) { - std::clog << "File too long to encrypt securely\n"; - std::exit(1); + if (file_size >= Aes_ctr_encryptor::MAX_CRYPT_BYTES) { + std::clog << "git-crypt: error: file too long to encrypt securely" << std::endl; + return 1; } - // 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 @@ -107,205 +576,910 @@ void clean (const char* keyfile) // looking up the nonce (which must be stored in the clear to allow for // decryption), we use an HMAC as opposed to a straight hash. - uint8_t digest[SHA1_LEN]; + // Note: Hmac_sha1_state::LEN >= Aes_ctr_encryptor::NONCE_LEN + + unsigned char digest[Hmac_sha1_state::LEN]; hmac.get(digest); // Write a header that... std::cout.write("\0GITCRYPT\0", 10); // ...identifies this as an encrypted file - std::cout.write(reinterpret_cast(digest), NONCE_LEN); // ...includes the nonce + std::cout.write(reinterpret_cast(digest), Aes_ctr_encryptor::NONCE_LEN); // ...includes the nonce // Now encrypt the file and write to stdout - aes_ctr_state state(digest, NONCE_LEN); + Aes_ctr_encryptor aes(key->aes_key, digest); // First read from the in-memory copy - const uint8_t* file_data = reinterpret_cast(file_contents.data()); - size_t file_data_len = file_contents.size(); - for (size_t i = 0; i < file_data_len; i += sizeof(buffer)) { - size_t buffer_len = std::min(sizeof(buffer), file_data_len - i); - state.process(&keys.enc, file_data + i, reinterpret_cast(buffer), buffer_len); + const unsigned char* file_data = reinterpret_cast(file_contents.data()); + size_t file_data_len = file_contents.size(); + while (file_data_len > 0) { + const size_t buffer_len = std::min(sizeof(buffer), file_data_len); + aes.process(file_data, reinterpret_cast(buffer), buffer_len); std::cout.write(buffer, buffer_len); + file_data += buffer_len; + file_data_len -= buffer_len; } // Then read from the temporary file if applicable if (temp_file.is_open()) { temp_file.seekg(0); - while (temp_file) { + while (temp_file.peek() != -1) { temp_file.read(buffer, sizeof(buffer)); - size_t buffer_len = temp_file.gcount(); + const size_t buffer_len = temp_file.gcount(); - state.process(&keys.enc, reinterpret_cast(buffer), reinterpret_cast(buffer), buffer_len); + aes.process(reinterpret_cast(buffer), + reinterpret_cast(buffer), + buffer_len); std::cout.write(buffer, buffer_len); } } + + return 0; +} + +static int decrypt_file_to_stdout (const Key_file& key_file, const unsigned char* header, std::istream& in) +{ + const unsigned char* nonce = header + 10; + uint32_t key_version = 0; // TODO: get the version from the file header + + const Key_file::Entry* key = key_file.get(key_version); + if (!key) { + std::clog << "git-crypt: error: key version " << key_version << " not available - please unlock with the latest version of the key." << std::endl; + return 1; + } + + 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; } // Decrypt contents of stdin and write to stdout -void smudge (const char* keyfile) +int smudge (int argc, const char** argv) { - keys_t keys; - load_keys(keyfile, &keys); + const char* key_name = 0; + const char* key_path = 0; + const char* legacy_key_path = 0; + + int argi = parse_plumbing_options(&key_name, &key_path, argc, argv); + if (argc - argi == 0) { + } else if (!key_name && !key_path && argc - argi == 1) { // Deprecated - for compatibility with pre-0.4 + legacy_key_path = argv[argi]; + } else { + std::clog << "Usage: git-crypt smudge [--key-name=NAME] [--key-file=PATH]" << std::endl; + return 2; + } + Key_file key_file; + load_key(key_file, key_name, key_path, legacy_key_path); // 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); + unsigned char header[10 + Aes_ctr_decryptor::NONCE_LEN]; + std::cin.read(reinterpret_cast(header), sizeof(header)); + 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; + std::clog << "git-crypt: Run 'git-crypt status' to make sure all files are properly encrypted." << std::endl; + std::clog << "git-crypt: If 'git-crypt status' reports no problems, then an older version of" << std::endl; + std::clog << "git-crypt: this file may be unencrypted in the repository's history. If this" << std::endl; + std::clog << "git-crypt: file contains sensitive information, you can use 'git filter-branch'" << std::endl; + std::clog << "git-crypt: to remove its old versions from the history." << std::endl; + std::cout.write(reinterpret_cast(header), std::cin.gcount()); // include the bytes which we already read + std::cout << std::cin.rdbuf(); + return 0; } - process_stream(std::cin, std::cout, &keys.enc, reinterpret_cast(header + 10)); + return decrypt_file_to_stdout(key_file, header, std::cin); } -void diff (const char* keyfile, const char* filename) +int diff (int argc, const char** argv) { - keys_t keys; - load_keys(keyfile, &keys); + const char* key_name = 0; + const char* key_path = 0; + const char* filename = 0; + const char* legacy_key_path = 0; + + int argi = parse_plumbing_options(&key_name, &key_path, argc, argv); + if (argc - argi == 1) { + filename = argv[argi]; + } else if (!key_name && !key_path && argc - argi == 2) { // Deprecated - for compatibility with pre-0.4 + legacy_key_path = argv[argi]; + filename = argv[argi + 1]; + } else { + std::clog << "Usage: git-crypt diff [--key-name=NAME] [--key-file=PATH] FILENAME" << std::endl; + return 2; + } + Key_file key_file; + load_key(key_file, key_name, key_path, legacy_key_path); // Open the file - std::ifstream in(filename); + std::ifstream in(filename, std::fstream::binary); if (!in) { - perror(filename); - std::exit(1); + std::clog << "git-crypt: " << filename << ": unable to open for reading" << std::endl; + return 1; } in.exceptions(std::fstream::badbit); // 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) { + unsigned char header[10 + Aes_ctr_decryptor::NONCE_LEN]; + in.read(reinterpret_cast(header), sizeof(header)); + if (in.gcount() != sizeof(header) || std::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; + std::cout.write(reinterpret_cast(header), in.gcount()); // include the bytes which we already read + std::cout << in.rdbuf(); + return 0; } - process_stream(in, std::cout, &keys.enc, reinterpret_cast(header + 10)); + // Go ahead and decrypt it + return decrypt_file_to_stdout(key_file, header, in); } - -void init (const char* argv0, const char* keyfile) +void help_init (std::ostream& out) { - if (access(keyfile, R_OK) == -1) { - perror(keyfile); - std::exit(1); - } - - // 0. Check to see if HEAD exists. See below why we do this. - bool head_exists = system("git rev-parse HEAD >/dev/null 2>/dev/null") == 0; + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt init [OPTIONS]" << std::endl; + out << std::endl; + out << " -k, --key-name KEYNAME Initialize the given key, instead of the default" << std::endl; + out << std::endl; +} +int init (int argc, const char** argv) +{ + const char* key_name = 0; + Options_list options; + options.push_back(Option_def("-k", &key_name)); + options.push_back(Option_def("--key-name", &key_name)); + + int argi = parse_options(options, argc, argv); + + if (!key_name && argc - argi == 1) { + std::clog << "Warning: 'git-crypt init' with a key file is deprecated as of git-crypt 0.4" << std::endl; + std::clog << "and will be removed in a future release. Please get in the habit of using" << std::endl; + std::clog << "'git-crypt unlock KEYFILE' instead." << std::endl; + return unlock(argc, argv); + } + if (argc - argi != 0) { + std::clog << "Error: git-crypt init takes no arguments" << std::endl; + help_init(std::clog); + return 2; + } + + if (key_name) { + validate_key_name_or_throw(key_name); + } + + std::string internal_key_path(get_internal_key_path(key_name)); + if (access(internal_key_path.c_str(), F_OK) == 0) { + // TODO: add a -f option to reinitialize the repo anyways (this should probably imply a refresh) + // TODO: include key_name in error message + std::clog << "Error: this repository has already been initialized with git-crypt." << std::endl; + return 1; + } + + // 1. Generate a key and install it + std::clog << "Generating key..." << std::endl; + Key_file key_file; + key_file.set_key_name(key_name); + key_file.generate(); + + mkdir_parent(internal_key_path); + if (!key_file.store_to_file(internal_key_path.c_str())) { + std::clog << "Error: " << internal_key_path << ": unable to write key file" << std::endl; + return 1; + } + + // 2. Configure git for git-crypt + configure_git_filters(key_name); + + return 0; +} + +void help_unlock (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt unlock" << std::endl; + out << " or: git-crypt unlock KEY_FILE ..." << std::endl; +} +int unlock (int argc, const char** argv) +{ // 1. Make sure working directory is clean (ignoring untracked files) - // We do this because we run 'git checkout -f HEAD' later and we don't - // want the user to lose any changes. 'git checkout -f HEAD' doesn't touch - // untracked files so it's safe to ignore those. - int status; + // We do this because we check out files later, and we don't want the + // user to lose any changes. (TODO: only care if encrypted files are + // modified, since we only check out encrypted files) + + // Running 'git status' also serves as a check that the Git repo is accessible. + std::stringstream status_output; - status = exec_command("git status -uno --porcelain", status_output); - if (status != 0) { - std::clog << "git status failed - is this a git repository?\n"; - std::exit(1); - } else if (status_output.peek() != -1 && head_exists) { - // We only care that the working directory is dirty if HEAD exists. - // If HEAD doesn't exist, we won't be resetting to it (see below) so - // it doesn't matter that the working directory is dirty. - std::clog << "Working directory not clean.\n"; - std::clog << "Please commit your changes or 'git stash' them before setting up git-crypt.\n"; - std::exit(1); + get_git_status(status_output); + if (status_output.peek() != -1) { + std::clog << "Error: Working directory not clean." << std::endl; + std::clog << "Please commit your changes or 'git stash' them before running 'git-crypt unlock'." << std::endl; + return 1; } // 2. Determine the path to the top of the repository. We pass this as the argument // to 'git checkout' below. (Determine the path now so in case it fails we haven't already // mucked with the git config.) - std::stringstream cdup_output; - if (exec_command("git rev-parse --show-cdup", cdup_output) != 0) { - std::clog << "git rev-parse --show-cdup failed\n"; - std::exit(1); - } + std::string path_to_top(get_path_to_top()); - // 3. Add config options to git + // 3. Load the key(s) + std::vector key_files; + if (argc > 0) { + // Read from the symmetric key file(s) - std::string git_crypt_path(std::strchr(argv0, '/') ? resolve_path(argv0) : argv0); - std::string keyfile_path(resolve_path(keyfile)); + for (int argi = 0; argi < argc; ++argi) { + const char* symmetric_key_file = argv[argi]; + Key_file key_file; - // git config filter.git-crypt.smudge "git-crypt smudge /path/to/key" - std::string command("git config filter.git-crypt.smudge "); - command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " smudge " + escape_shell_arg(keyfile_path)); - - if (system(command.c_str()) != 0) { - std::clog << "git config failed\n"; - std::exit(1); - } + try { + if (std::strcmp(symmetric_key_file, "-") == 0) { + key_file.load(std::cin); + } else { + if (!key_file.load_from_file(symmetric_key_file)) { + std::clog << "Error: " << symmetric_key_file << ": unable to read key file" << std::endl; + return 1; + } + } + } catch (Key_file::Incompatible) { + std::clog << "Error: " << symmetric_key_file << " is in an incompatible format" << std::endl; + std::clog << "Please upgrade to a newer version of git-crypt." << std::endl; + return 1; + } catch (Key_file::Malformed) { + std::clog << "Error: " << symmetric_key_file << ": not a valid git-crypt key file" << std::endl; + std::clog << "If this key was created prior to git-crypt 0.4, you need to migrate it" << std::endl; + std::clog << "by running 'git-crypt migrate-key /path/to/old_key /path/to/migrated_key'." << std::endl; + return 1; + } - // git config filter.git-crypt.clean "git-crypt clean /path/to/key" - command = "git config filter.git-crypt.clean "; - command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " clean " + escape_shell_arg(keyfile_path)); - - if (system(command.c_str()) != 0) { - std::clog << "git config failed\n"; - std::exit(1); - } - - // git config diff.git-crypt.textconv "git-crypt diff /path/to/key" - command = "git config diff.git-crypt.textconv "; - command += escape_shell_arg(escape_shell_arg(git_crypt_path) + " diff " + escape_shell_arg(keyfile_path)); - - if (system(command.c_str()) != 0) { - std::clog << "git config failed\n"; - std::exit(1); - } - - - // 4. Do a force checkout so any files that were previously checked out encrypted - // will now be checked out decrypted. - // If HEAD doesn't exist (perhaps because this repo doesn't have any files yet) - // just skip the checkout. - if (head_exists) { - std::string path_to_top; - std::getline(cdup_output, path_to_top); - - command = "git checkout -f HEAD -- "; - if (path_to_top.empty()) { - command += "."; - } else { - command += escape_shell_arg(path_to_top); + key_files.push_back(key_file); } - - if (system(command.c_str()) != 0) { - std::clog << "git checkout failed\n"; - std::clog << "git-crypt has been set up but existing encrypted files have not been decrypted\n"; - std::exit(1); + } else { + // Decrypt GPG key from root of repo + std::string repo_keys_path(get_repo_keys_path()); + std::vector gpg_secret_keys(gpg_list_secret_keys()); + // TODO: command-line option to specify the precise secret key to use + // TODO: don't hard code key version 0 here - instead, determine the most recent version and try to decrypt that, or decrypt all versions if command-line option specified + // TODO: command line option to only unlock specific key instead of all of them + // TODO: avoid decrypting repo keys which are already unlocked in the .git directory + if (!decrypt_repo_keys(key_files, 0, gpg_secret_keys, repo_keys_path)) { + std::clog << "Error: no GPG secret key available to unlock this repository." << std::endl; + std::clog << "To unlock with a shared symmetric key instead, specify the path to the symmetric key as an argument to 'git-crypt unlock'." << std::endl; + // TODO std::clog << "To see a list of GPG keys authorized to unlock this repository, run 'git-crypt ls-gpg-users'." << std::endl; + return 1; } } + + + // 4. Install the key(s) and configure the git filters + std::vector encrypted_files; + for (std::vector::iterator key_file(key_files.begin()); key_file != key_files.end(); ++key_file) { + std::string internal_key_path(get_internal_key_path(key_file->get_key_name())); + // TODO: croak if internal_key_path already exists??? + mkdir_parent(internal_key_path); + if (!key_file->store_to_file(internal_key_path.c_str())) { + std::clog << "Error: " << internal_key_path << ": unable to write key file" << std::endl; + return 1; + } + + configure_git_filters(key_file->get_key_name()); + get_encrypted_files(encrypted_files, key_file->get_key_name()); + } + + // 5. Check out the files that are currently encrypted. + // Git won't check out a file if its mtime hasn't changed, so touch every file first. + for (std::vector::const_iterator file(encrypted_files.begin()); file != encrypted_files.end(); ++file) { + touch_file(*file); + } + if (!git_checkout(encrypted_files)) { + std::clog << "Error: 'git checkout' failed" << std::endl; + std::clog << "git-crypt has been set up but existing encrypted files have not been decrypted" << std::endl; + return 1; + } + + return 0; } -void keygen (const char* keyfile) +void help_lock (std::ostream& out) { - if (access(keyfile, F_OK) == 0) { - std::clog << keyfile << ": File already exists - please remove before continuing\n"; - std::exit(1); - } - mode_t old_umask = umask(0077); // make sure key file is protected - std::ofstream keyout(keyfile); - if (!keyout) { - perror(keyfile); - std::exit(1); - } - umask(old_umask); - - std::clog << "Generating key...\n"; - std::clog.flush(); - unsigned char buffer[AES_KEY_BITS/8 + HMAC_KEY_LEN]; - if (RAND_bytes(buffer, sizeof(buffer)) != 1) { - while (unsigned long code = ERR_get_error()) { - char error_string[120]; - ERR_error_string_n(code, error_string, sizeof(error_string)); - std::clog << "Error: " << error_string << '\n'; - } - std::exit(1); - } - keyout.write(reinterpret_cast(buffer), sizeof(buffer)); + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt lock [OPTIONS]" << std::endl; + out << std::endl; + out << " -a, --all Lock all keys, instead of just the default" << std::endl; + out << " -k, --key-name KEYNAME Lock the given key, instead of the default" << std::endl; + out << std::endl; } +int lock (int argc, const char** argv) +{ + const char* key_name = 0; + bool all_keys = false; + Options_list options; + options.push_back(Option_def("-k", &key_name)); + options.push_back(Option_def("--key-name", &key_name)); + options.push_back(Option_def("-a", &all_keys)); + options.push_back(Option_def("--all", &all_keys)); + + int argi = parse_options(options, argc, argv); + + if (argc - argi != 0) { + std::clog << "Error: git-crypt lock takes no arguments" << std::endl; + help_lock(std::clog); + return 2; + } + + if (all_keys && key_name) { + std::clog << "Error: -k and --all options are mutually exclusive" << std::endl; + return 2; + } + + // 1. Make sure working directory is clean (ignoring untracked files) + // We do this because we check out files later, and we don't want the + // user to lose any changes. (TODO: only care if encrypted files are + // modified, since we only check out encrypted files) + + // Running 'git status' also serves as a check that the Git repo is accessible. + + std::stringstream status_output; + get_git_status(status_output); + if (status_output.peek() != -1) { + std::clog << "Error: Working directory not clean." << std::endl; + std::clog << "Please commit your changes or 'git stash' them before running 'git-crypt lock'." << std::endl; + return 1; + } + + // 2. Determine the path to the top of the repository. We pass this as the argument + // to 'git checkout' below. (Determine the path now so in case it fails we haven't already + // mucked with the git config.) + std::string path_to_top(get_path_to_top()); + + // 3. unconfigure the git filters and remove decrypted keys + std::vector encrypted_files; + if (all_keys) { + // unconfigure for all keys + std::vector dirents = get_directory_contents(get_internal_keys_path().c_str()); + + for (std::vector::const_iterator dirent(dirents.begin()); dirent != dirents.end(); ++dirent) { + const char* this_key_name = (*dirent == "default" ? 0 : dirent->c_str()); + remove_file(get_internal_key_path(this_key_name)); + unconfigure_git_filters(this_key_name); + get_encrypted_files(encrypted_files, this_key_name); + } + } else { + // just handle the given key + std::string internal_key_path(get_internal_key_path(key_name)); + if (access(internal_key_path.c_str(), F_OK) == -1 && errno == ENOENT) { + std::clog << "Error: this repository is not currently locked"; + if (key_name) { + std::clog << " with key '" << key_name << "'"; + } + std::clog << "." << std::endl; + return 1; + } + + remove_file(internal_key_path); + unconfigure_git_filters(key_name); + get_encrypted_files(encrypted_files, key_name); + } + + // 4. Check out the files that are currently decrypted but should be encrypted. + // Git won't check out a file if its mtime hasn't changed, so touch every file first. + for (std::vector::const_iterator file(encrypted_files.begin()); file != encrypted_files.end(); ++file) { + touch_file(*file); + } + if (!git_checkout(encrypted_files)) { + std::clog << "Error: 'git checkout' failed" << std::endl; + std::clog << "git-crypt has been locked but up but existing decrypted files have not been encrypted" << std::endl; + return 1; + } + + return 0; +} + +void help_add_gpg_user (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt add-gpg-user [OPTIONS] GPG_USER_ID ..." << std::endl; + out << std::endl; + out << " -k, --key-name KEYNAME Add GPG user to given key, instead of default" << std::endl; + out << " -n, --no-commit Don't automatically commit" << std::endl; + out << std::endl; +} +int add_gpg_user (int argc, const char** argv) +{ + const char* key_name = 0; + bool no_commit = false; + Options_list options; + options.push_back(Option_def("-k", &key_name)); + options.push_back(Option_def("--key-name", &key_name)); + options.push_back(Option_def("-n", &no_commit)); + options.push_back(Option_def("--no-commit", &no_commit)); + + int argi = parse_options(options, argc, argv); + if (argc - argi == 0) { + std::clog << "Error: no GPG user ID specified" << std::endl; + help_add_gpg_user(std::clog); + return 2; + } + + // build a list of key fingerprints for every collaborator specified on the command line + std::vector collab_keys; + + for (int i = argi; i < argc; ++i) { + std::vector keys(gpg_lookup_key(argv[i])); + if (keys.empty()) { + std::clog << "Error: public key for '" << argv[i] << "' not found in your GPG keyring" << std::endl; + return 1; + } + if (keys.size() > 1) { + std::clog << "Error: more than one public key matches '" << argv[i] << "' - please be more specific" << std::endl; + return 1; + } + collab_keys.push_back(keys[0]); + } + + // TODO: have a retroactive option to grant access to all key versions, not just the most recent + Key_file key_file; + load_key(key_file, key_name); + const Key_file::Entry* key = key_file.get_latest(); + if (!key) { + std::clog << "Error: key file is empty" << std::endl; + return 1; + } + + const std::string state_path(get_repo_state_path()); + std::vector new_files; + + encrypt_repo_key(key_name, *key, collab_keys, get_repo_keys_path(state_path), &new_files); + + // Add a .gitatributes file to the repo state directory to prevent files in it from being encrypted. + const std::string state_gitattributes_path(state_path + "/.gitattributes"); + if (access(state_gitattributes_path.c_str(), F_OK) != 0) { + std::ofstream state_gitattributes_file(state_gitattributes_path.c_str()); + state_gitattributes_file << "* !filter !diff\n"; + state_gitattributes_file.close(); + if (!state_gitattributes_file) { + std::clog << "Error: unable to write " << state_gitattributes_path << std::endl; + return 1; + } + new_files.push_back(state_gitattributes_path); + } + + // add/commit the new files + if (!new_files.empty()) { + // git add NEW_FILE ... + std::vector command; + command.push_back("git"); + command.push_back("add"); + command.push_back("--"); + command.insert(command.end(), new_files.begin(), new_files.end()); + if (!successful_exit(exec_command(command))) { + std::clog << "Error: 'git add' failed" << std::endl; + return 1; + } + + // git commit ... + if (!no_commit) { + // TODO: include key_name in commit message + std::ostringstream commit_message_builder; + commit_message_builder << "Add " << collab_keys.size() << " git-crypt collaborator" << (collab_keys.size() != 1 ? "s" : "") << "\n\nNew collaborators:\n\n"; + for (std::vector::const_iterator collab(collab_keys.begin()); collab != collab_keys.end(); ++collab) { + commit_message_builder << '\t' << gpg_shorten_fingerprint(*collab) << ' ' << gpg_get_uid(*collab) << '\n'; + } + + // git commit -m MESSAGE NEW_FILE ... + command.clear(); + command.push_back("git"); + command.push_back("commit"); + command.push_back("-m"); + command.push_back(commit_message_builder.str()); + command.push_back("--"); + command.insert(command.end(), new_files.begin(), new_files.end()); + + if (!successful_exit(exec_command(command))) { + std::clog << "Error: 'git commit' failed" << std::endl; + return 1; + } + } + } + + return 0; +} + +void help_rm_gpg_user (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt rm-gpg-user [OPTIONS] GPG_USER_ID ..." << std::endl; + out << std::endl; + out << " -k, --key-name KEYNAME Remove user from given key, instead of default" << std::endl; + out << " -n, --no-commit Don't automatically commit" << std::endl; + out << std::endl; +} +int rm_gpg_user (int argc, const char** argv) // TODO +{ + std::clog << "Error: rm-gpg-user is not yet implemented." << std::endl; + return 1; +} + +void help_ls_gpg_users (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt ls-gpg-users" << std::endl; +} +int ls_gpg_users (int argc, const char** argv) // TODO +{ + // Sketch: + // Scan the sub-directories in .git-crypt/keys, outputting something like this: + // ==== + // Key version 0: + // 0x143DE9B3F7316900 Andrew Ayer + // 0x4E386D9C9C61702F ??? + // Key version 1: + // 0x143DE9B3F7316900 Andrew Ayer + // 0x1727274463D27F40 John Smith + // 0x4E386D9C9C61702F ??? + // ==== + // To resolve a long hex ID, use a command like this: + // gpg --options /dev/null --fixed-list-mode --batch --with-colons --list-keys 0x143DE9B3F7316900 + + std::clog << "Error: ls-gpg-users is not yet implemented." << std::endl; + return 1; +} + +void help_export_key (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt export-key [OPTIONS] FILENAME" << std::endl; + out << std::endl; + out << " -k, --key-name KEYNAME Export the given key, instead of the default" << std::endl; + out << std::endl; + out << "When FILENAME is -, export to standard out." << std::endl; +} +int export_key (int argc, const char** argv) +{ + // TODO: provide options to export only certain key versions + const char* key_name = 0; + Options_list options; + options.push_back(Option_def("-k", &key_name)); + options.push_back(Option_def("--key-name", &key_name)); + + int argi = parse_options(options, argc, argv); + + if (argc - argi != 1) { + std::clog << "Error: no filename specified" << std::endl; + help_export_key(std::clog); + return 2; + } + + Key_file key_file; + load_key(key_file, key_name); + + const char* out_file_name = argv[argi]; + + if (std::strcmp(out_file_name, "-") == 0) { + key_file.store(std::cout); + } else { + if (!key_file.store_to_file(out_file_name)) { + std::clog << "Error: " << out_file_name << ": unable to write key file" << std::endl; + return 1; + } + } + + return 0; +} + +void help_keygen (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt keygen FILENAME" << std::endl; + out << std::endl; + out << "When FILENAME is -, write to standard out." << std::endl; +} +int keygen (int argc, const char** argv) +{ + if (argc != 1) { + std::clog << "Error: no filename specified" << std::endl; + help_keygen(std::clog); + return 2; + } + + const char* key_file_name = argv[0]; + + if (std::strcmp(key_file_name, "-") != 0 && access(key_file_name, F_OK) == 0) { + std::clog << key_file_name << ": File already exists" << std::endl; + return 1; + } + + std::clog << "Generating key..." << std::endl; + Key_file key_file; + key_file.generate(); + + if (std::strcmp(key_file_name, "-") == 0) { + key_file.store(std::cout); + } else { + if (!key_file.store_to_file(key_file_name)) { + std::clog << "Error: " << key_file_name << ": unable to write key file" << std::endl; + return 1; + } + } + return 0; +} + +void help_migrate_key (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt migrate-key OLDFILENAME NEWFILENAME" << std::endl; + out << std::endl; + out << "Use - to read from standard in/write to standard out." << std::endl; +} +int migrate_key (int argc, const char** argv) +{ + if (argc != 2) { + std::clog << "Error: filenames not specified" << std::endl; + help_migrate_key(std::clog); + return 2; + } + + const char* key_file_name = argv[0]; + const char* new_key_file_name = argv[1]; + Key_file key_file; + + try { + if (std::strcmp(key_file_name, "-") == 0) { + key_file.load_legacy(std::cin); + } else { + std::ifstream in(key_file_name, std::fstream::binary); + if (!in) { + std::clog << "Error: " << key_file_name << ": unable to open for reading" << std::endl; + return 1; + } + key_file.load_legacy(in); + } + + if (std::strcmp(new_key_file_name, "-") == 0) { + key_file.store(std::cout); + } else { + if (!key_file.store_to_file(new_key_file_name)) { + std::clog << "Error: " << new_key_file_name << ": unable to write key file" << std::endl; + return 1; + } + } + } catch (Key_file::Malformed) { + std::clog << "Error: " << key_file_name << ": not a valid legacy git-crypt key file" << std::endl; + return 1; + } + + return 0; +} + +void help_refresh (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt refresh" << std::endl; +} +int refresh (int argc, const char** argv) // TODO: do a force checkout, much like in unlock +{ + std::clog << "Error: refresh is not yet implemented." << std::endl; + return 1; +} + +void help_status (std::ostream& out) +{ + // |--------------------------------------------------------------------------------| 80 chars + out << "Usage: git-crypt status [OPTIONS] [FILE ...]" << std::endl; + //out << " or: git-crypt status -r [OPTIONS]" << std::endl; + //out << " or: git-crypt status -f" << std::endl; + out << std::endl; + out << " -e Show encrypted files only" << std::endl; + out << " -u Show unencrypted files only" << std::endl; + //out << " -r Show repository status only" << std::endl; + out << " -f, --fix Fix problems with the repository" << std::endl; + //out << " -z Machine-parseable output" << std::endl; + out << std::endl; +} +int status (int argc, const char** argv) +{ + // Usage: + // git-crypt status -r [-z] Show repo status + // git-crypt status [-e | -u] [-z] [FILE ...] Show encrypted status of files + // git-crypt status -f Fix unencrypted blobs + + bool repo_status_only = false; // -r show repo status only + bool show_encrypted_only = false; // -e show encrypted files only + bool show_unencrypted_only = false; // -u show unencrypted files only + bool fix_problems = false; // -f fix problems + bool machine_output = false; // -z machine-parseable output + + Options_list options; + options.push_back(Option_def("-r", &repo_status_only)); + options.push_back(Option_def("-e", &show_encrypted_only)); + options.push_back(Option_def("-u", &show_unencrypted_only)); + options.push_back(Option_def("-f", &fix_problems)); + options.push_back(Option_def("--fix", &fix_problems)); + options.push_back(Option_def("-z", &machine_output)); + + int argi = parse_options(options, argc, argv); + + if (repo_status_only) { + if (show_encrypted_only || show_unencrypted_only) { + std::clog << "Error: -e and -u options cannot be used with -r" << std::endl; + return 2; + } + if (fix_problems) { + std::clog << "Error: -f option cannot be used with -r" << std::endl; + return 2; + } + if (argc - argi != 0) { + std::clog << "Error: filenames cannot be specified when -r is used" << std::endl; + return 2; + } + } + + if (show_encrypted_only && show_unencrypted_only) { + std::clog << "Error: -e and -u options are mutually exclusive" << std::endl; + return 2; + } + + if (fix_problems && (show_encrypted_only || show_unencrypted_only)) { + std::clog << "Error: -e and -u options cannot be used with -f" << std::endl; + return 2; + } + + if (machine_output) { + // TODO: implement machine-parseable output + std::clog << "Sorry, machine-parseable output is not yet implemented" << std::endl; + return 2; + } + + if (argc - argi == 0) { + // TODO: check repo status: + // is it set up for git-crypt? + // which keys are unlocked? + // --> check for filter config (see configure_git_filters()) and corresponding internal key + + if (repo_status_only) { + return 0; + } + } + + // git ls-files -cotsz --exclude-standard ... + std::vector command; + command.push_back("git"); + command.push_back("ls-files"); + command.push_back("-cotsz"); + command.push_back("--exclude-standard"); + command.push_back("--"); + if (argc - argi == 0) { + const std::string path_to_top(get_path_to_top()); + if (!path_to_top.empty()) { + command.push_back(path_to_top); + } + } else { + for (int i = argi; i < argc; ++i) { + command.push_back(argv[i]); + } + } + + std::stringstream output; + if (!successful_exit(exec_command(command, output))) { + throw Error("'git ls-files' failed - is this a Git repository?"); + } + + // Output looks like (w/o newlines): + // ? .gitignore\0 + // H 100644 06ec22e5ed0de9280731ef000a10f9c3fbc26338 0 afile\0 + + std::vector files; + bool attribute_errors = false; + bool unencrypted_blob_errors = false; + unsigned int nbr_of_fixed_blobs = 0; + unsigned int nbr_of_fix_errors = 0; + + while (output.peek() != -1) { + std::string tag; + std::string object_id; + std::string filename; + output >> tag; + if (tag != "?") { + std::string mode; + std::string stage; + output >> mode >> object_id >> stage; + } + output >> std::ws; + std::getline(output, filename, '\0'); + + // TODO: get file attributes en masse for efficiency... unfortunately this requires machine-parseable output from git check-attr to be workable, and this is only supported in Git 1.8.5 and above (released 27 Nov 2013) + const std::pair file_attrs(get_file_attributes(filename)); + + 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 (fix_problems && blob_is_unencrypted) { + if (access(filename.c_str(), F_OK) != 0) { + std::clog << "Error: " << filename << ": cannot stage encrypted version because not present in working tree - please 'git rm' or 'git checkout' it" << std::endl; + ++nbr_of_fix_errors; + } else { + touch_file(filename); + std::vector git_add_command; + git_add_command.push_back("git"); + git_add_command.push_back("add"); + git_add_command.push_back("--"); + git_add_command.push_back(filename); + if (!successful_exit(exec_command(git_add_command))) { + throw Error("'git-add' failed"); + } + if (check_if_file_is_encrypted(filename)) { + std::cout << filename << ": staged encrypted version" << std::endl; + ++nbr_of_fixed_blobs; + } else { + std::clog << "Error: " << filename << ": still unencrypted even after staging" << std::endl; + ++nbr_of_fix_errors; + } + } + } else if (!fix_problems && !show_unencrypted_only) { + // TODO: output the key name used to encrypt this file + std::cout << " encrypted: " << filename; + if (file_attrs.second != file_attrs.first) { + // but diff filter is not properly set + std::cout << " *** WARNING: diff=" << file_attrs.first << " attribute not set ***"; + attribute_errors = true; + } + if (blob_is_unencrypted) { + // File not actually encrypted + std::cout << " *** WARNING: staged/committed version is NOT ENCRYPTED! ***"; + unencrypted_blob_errors = true; + } + std::cout << std::endl; + } + } else { + // File not encrypted + if (!fix_problems && !show_encrypted_only) { + std::cout << "not encrypted: " << filename << std::endl; + } + } + } + + int exit_status = 0; + + if (attribute_errors) { + std::cout << std::endl; + std::cout << "Warning: one or more files has a git-crypt filter attribute but not a" << std::endl; + std::cout << "corresponding git-crypt diff attribute. For proper 'git diff' operation" << std::endl; + std::cout << "you should fix the .gitattributes file to specify the correct diff attribute." << std::endl; + std::cout << "Consult the git-crypt documentation for help." << std::endl; + exit_status = 1; + } + if (unencrypted_blob_errors) { + std::cout << std::endl; + std::cout << "Warning: one or more files is marked for encryption via .gitattributes but" << std::endl; + std::cout << "was staged and/or committed before the .gitattributes file was in effect." << std::endl; + std::cout << "Run 'git-crypt status' with the '-f' option to stage an encrypted version." << std::endl; + exit_status = 1; + } + if (nbr_of_fixed_blobs) { + std::cout << "Staged " << nbr_of_fixed_blobs << " encrypted file" << (nbr_of_fixed_blobs != 1 ? "s" : "") << "." << std::endl; + std::cout << "Warning: if these files were previously committed, unencrypted versions still exist in the repository's history." << std::endl; + } + if (nbr_of_fix_errors) { + std::cout << "Unable to stage " << nbr_of_fix_errors << " file" << (nbr_of_fix_errors != 1 ? "s" : "") << "." << std::endl; + exit_status = 1; + } + + return exit_status; +} + diff --git a/commands.hpp b/commands.hpp index ce68129..32caa0f 100644 --- a/commands.hpp +++ b/commands.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -28,15 +28,46 @@ * as that of the covered work. */ -#ifndef _COMMANDS_H -#define _COMMANDS_H +#ifndef GIT_CRYPT_COMMANDS_HPP +#define GIT_CRYPT_COMMANDS_HPP +#include +#include -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); +struct Error { + std::string message; + + explicit Error (std::string m) : message(m) { } +}; + +// Plumbing commands: +int clean (int argc, const char** argv); +int smudge (int argc, const char** argv); +int diff (int argc, const char** argv); +// Public commands: +int init (int argc, const char** argv); +int unlock (int argc, const char** argv); +int lock (int argc, const char** argv); +int add_gpg_user (int argc, const char** argv); +int rm_gpg_user (int argc, const char** argv); +int ls_gpg_users (int argc, const char** argv); +int export_key (int argc, const char** argv); +int keygen (int argc, const char** argv); +int migrate_key (int argc, const char** argv); +int refresh (int argc, const char** argv); +int status (int argc, const char** argv); + +// Help messages: +void help_init (std::ostream&); +void help_unlock (std::ostream&); +void help_lock (std::ostream&); +void help_add_gpg_user (std::ostream&); +void help_rm_gpg_user (std::ostream&); +void help_ls_gpg_users (std::ostream&); +void help_export_key (std::ostream&); +void help_keygen (std::ostream&); +void help_migrate_key (std::ostream&); +void help_refresh (std::ostream&); +void help_status (std::ostream&); #endif - diff --git a/crypto-openssl.cpp b/crypto-openssl.cpp new file mode 100644 index 0000000..6483e86 --- /dev/null +++ b/crypto-openssl.cpp @@ -0,0 +1,115 @@ +/* + * Copyright 2012, 2014 Andrew Ayer + * + * This file is part of git-crypt. + * + * git-crypt is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * git-crypt is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with git-crypt. If not, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify the Program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, the licensors of the Program + * grant you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#include "crypto.hpp" +#include "key.hpp" +#include "util.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +void init_crypto () +{ + ERR_load_crypto_strings(); +} + +struct Aes_ecb_encryptor::Aes_impl { + AES_KEY key; +}; + +Aes_ecb_encryptor::Aes_ecb_encryptor (const unsigned char* raw_key) +: impl(new Aes_impl) +{ + if (AES_set_encrypt_key(raw_key, KEY_LEN * 8, &(impl->key)) != 0) { + throw Crypto_error("Aes_ctr_encryptor::Aes_ctr_encryptor", "AES_set_encrypt_key failed"); + } +} + +Aes_ecb_encryptor::~Aes_ecb_encryptor () +{ + // Note: Explicit destructor necessary because class contains an auto_ptr + // which contains an incomplete type when the auto_ptr is declared. + + explicit_memset(&impl->key, '\0', sizeof(impl->key)); +} + +void Aes_ecb_encryptor::encrypt(const unsigned char* plain, unsigned char* cipher) +{ + AES_encrypt(plain, cipher, &(impl->key)); +} + +struct Hmac_sha1_state::Hmac_impl { + HMAC_CTX ctx; +}; + +Hmac_sha1_state::Hmac_sha1_state (const unsigned char* key, size_t key_len) +: impl(new Hmac_impl) +{ + HMAC_Init(&(impl->ctx), key, key_len, EVP_sha1()); +} + +Hmac_sha1_state::~Hmac_sha1_state () +{ + // Note: Explicit destructor necessary because class contains an auto_ptr + // which contains an incomplete type when the auto_ptr is declared. + + HMAC_cleanup(&(impl->ctx)); +} + +void Hmac_sha1_state::add (const unsigned char* buffer, size_t buffer_len) +{ + HMAC_Update(&(impl->ctx), buffer, buffer_len); +} + +void Hmac_sha1_state::get (unsigned char* digest) +{ + unsigned int len; + HMAC_Final(&(impl->ctx), digest, &len); +} + + +void random_bytes (unsigned char* buffer, size_t len) +{ + if (RAND_bytes(buffer, len) != 1) { + std::ostringstream message; + while (unsigned long code = ERR_get_error()) { + char error_string[120]; + ERR_error_string_n(code, error_string, sizeof(error_string)); + message << "OpenSSL Error: " << error_string << "; "; + } + throw Crypto_error("random_bytes", message.str()); + } +} + diff --git a/crypto.cpp b/crypto.cpp index e1a8594..3ae3ecb 100644 --- a/crypto.cpp +++ b/crypto.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -28,102 +28,54 @@ * as that of the covered work. */ -#define _BSD_SOURCE #include "crypto.hpp" -#include -#include -#include -#include -#include -#include +#include "util.hpp" #include -#include -#include -void load_keys (const char* filepath, keys_t* keys) +Aes_ctr_encryptor::Aes_ctr_encryptor (const unsigned char* raw_key, const unsigned char* nonce) +: ecb(raw_key) { - 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))); + // Set first 12 bytes of the CTR value to the nonce. + // This stays the same for the entirety of this object's lifetime. + std::memcpy(ctr_value, nonce, NONCE_LEN); byte_counter = 0; - memset(otp, '\0', sizeof(otp)); } -void aes_ctr_state::process (const AES_KEY* key, const uint8_t* in, uint8_t* out, size_t len) +Aes_ctr_encryptor::~Aes_ctr_encryptor () +{ + explicit_memset(pad, '\0', BLOCK_LEN); +} + +void Aes_ctr_encryptor::process (const unsigned char* in, unsigned char* 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 = htonl(byte_counter / 16); - memcpy(ctr, nonce, 12); - memcpy(ctr + 12, &blockno, 4); - AES_encrypt(ctr, otp, key); + if (byte_counter % BLOCK_LEN == 0) { + // Set last 4 bytes of CTR to the (big-endian) block number (sequentially increasing with each block) + store_be32(ctr_value + NONCE_LEN, byte_counter / BLOCK_LEN); + + // Generate a new pad + ecb.encrypt(ctr_value, pad); } // encrypt one byte - out[i] = in[i] ^ otp[byte_counter++ % 16]; + out[i] = in[i] ^ pad[byte_counter++ % BLOCK_LEN]; + + if (byte_counter == 0) { + throw Crypto_error("Aes_ctr_encryptor::process", "Too much data to encrypt securely"); + } } } -hmac_sha1_state::hmac_sha1_state (const uint8_t* key, size_t key_len) -{ - HMAC_Init(&ctx, key, key_len, EVP_sha1()); -} - -hmac_sha1_state::~hmac_sha1_state () -{ - HMAC_cleanup(&ctx); -} - -void hmac_sha1_state::add (const uint8_t* buffer, size_t buffer_len) -{ - HMAC_Update(&ctx, buffer, buffer_len); -} - -void hmac_sha1_state::get (uint8_t* digest) -{ - unsigned int len; - HMAC_Final(&ctx, digest, &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) +void Aes_ctr_encryptor::process_stream (std::istream& in, std::ostream& out, const unsigned char* key, const unsigned char* nonce) { - aes_ctr_state state(nonce, 12); + Aes_ctr_encryptor aes(key, nonce); - uint8_t buffer[1024]; + unsigned char buffer[1024]; while (in) { in.read(reinterpret_cast(buffer), sizeof(buffer)); - state.process(enc_key, buffer, buffer, in.gcount()); + aes.process(buffer, buffer, in.gcount()); out.write(reinterpret_cast(buffer), in.gcount()); } } + diff --git a/crypto.hpp b/crypto.hpp index e8166e2..db03241 100644 --- a/crypto.hpp +++ b/crypto.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -28,56 +28,90 @@ * as that of the covered work. */ -#ifndef _CRYPTO_H -#define _CRYPTO_H +#ifndef GIT_CRYPT_CRYPTO_HPP +#define GIT_CRYPT_CRYPTO_HPP -#include -#include +#include "key.hpp" #include #include #include +#include +#include -enum { - SHA1_LEN = 20, - NONCE_LEN = 12, - 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 +void init_crypto (); + +struct Crypto_error { + std::string where; + std::string message; + + Crypto_error (const std::string& w, const std::string& m) : where(w), message(m) { } }; -struct keys_t { - AES_KEY enc; - uint8_t hmac[HMAC_KEY_LEN]; -}; -void load_keys (const char* filepath, keys_t* keys); +class Aes_ecb_encryptor { +public: + enum { + KEY_LEN = AES_KEY_LEN, + BLOCK_LEN = 16 + }; -class aes_ctr_state { - char nonce[NONCE_LEN];// 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 +private: + struct Aes_impl; + + std::auto_ptr impl; public: - aes_ctr_state (const uint8_t* arg_nonce, size_t arg_nonce_len); - - void process (const AES_KEY* key, const uint8_t* in, uint8_t* out, size_t len); + Aes_ecb_encryptor (const unsigned char* key); + ~Aes_ecb_encryptor (); + void encrypt (const unsigned char* plain, unsigned char* cipher); }; -class hmac_sha1_state { - HMAC_CTX ctx; - - // disallow copy/assignment: - hmac_sha1_state (const hmac_sha1_state&) { } - hmac_sha1_state& operator= (const hmac_sha1_state&) { return *this; } +class Aes_ctr_encryptor { public: - hmac_sha1_state (const uint8_t* key, size_t key_len); - ~hmac_sha1_state (); + enum { + NONCE_LEN = 12, + KEY_LEN = AES_KEY_LEN, + BLOCK_LEN = 16, + MAX_CRYPT_BYTES = (1ULL<<32)*16 // Don't encrypt more than this or the CTR value will repeat itself + }; - void add (const uint8_t* buffer, size_t buffer_len); - void get (uint8_t*); +private: + Aes_ecb_encryptor ecb; + unsigned char ctr_value[BLOCK_LEN]; // Current CTR value (used as input to AES to derive pad) + unsigned char pad[BLOCK_LEN]; // Current encryption pad (output of AES) + uint32_t byte_counter; // How many bytes processed so far? + +public: + Aes_ctr_encryptor (const unsigned char* key, const unsigned char* nonce); + ~Aes_ctr_encryptor (); + + void process (const unsigned char* in, unsigned char* out, size_t len); + + // Encrypt/decrypt an entire input stream, writing to the given output stream + static void process_stream (std::istream& in, std::ostream& out, const unsigned char* key, const unsigned char* nonce); }; -// 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); +typedef Aes_ctr_encryptor Aes_ctr_decryptor; +class Hmac_sha1_state { +public: + enum { + LEN = 20, + KEY_LEN = HMAC_KEY_LEN + }; + +private: + struct Hmac_impl; + + std::auto_ptr impl; + +public: + Hmac_sha1_state (const unsigned char* key, size_t key_len); + ~Hmac_sha1_state (); + + void add (const unsigned char* buffer, size_t buffer_len); + void get (unsigned char*); +}; + +void random_bytes (unsigned char*, size_t); #endif diff --git a/doc/multiple_keys.md b/doc/multiple_keys.md new file mode 100644 index 0000000..6d7fc69 --- /dev/null +++ b/doc/multiple_keys.md @@ -0,0 +1,24 @@ +In addition to the implicit default key, git-crypt supports alternative +keys which can be used to encrypt specific files and can be shared with +specific GPG users. This is useful if you want to grant different +collaborators access to different sets of files. + +To generate an alternative key named KEYNAME, pass the `-k KEYNAME` +option to `git-crypt init` as follows: + + git-crypt init -k KEYNAME + +To encrypt a file with an alternative key, use the `git-crypt-KEYNAME` +filter in `.gitattributes` as follows: + + secretfile filter=git-crypt-KEYNAME diff=git-crypt-KEYNAME + +To export an alternative key or share it with a GPG user, pass the `-k +KEYNAME` option to `git-crypt export-key` or `git-crypt add-gpg-user` +as follows: + + git-crypt export-key -k KEYNAME /path/to/keyfile + git-crypt add-gpg-user -k KEYNAME GPG_USER_ID + +To unlock a repository with an alternative key, use `git-crypt unlock` +normally. git-crypt will automatically determine which key is being used. diff --git a/git-crypt.cpp b/git-crypt.cpp index bd58391..9505834 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -28,59 +28,236 @@ * as that of the covered work. */ +#include "git-crypt.hpp" #include "commands.hpp" #include "util.hpp" +#include "crypto.hpp" +#include "key.hpp" +#include "gpg.hpp" +#include "parse_options.hpp" #include +#include #include -#include +#include -static void print_usage (const char* argv0) +const char* argv0; + +static void print_usage (std::ostream& out) { - 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"; + out << "Usage: " << argv0 << " COMMAND [ARGS ...]" << std::endl; + out << std::endl; + // |--------------------------------------------------------------------------------| 80 characters + out << "Common commands:" << std::endl; + out << " init generate a key and prepare repo to use git-crypt" << std::endl; + out << " status display which files are encrypted" << std::endl; + //out << " refresh ensure all files in the repo are properly decrypted" << std::endl; + out << " lock de-configure git-crypt and re-encrypt files in work tree" << std::endl; + out << std::endl; + out << "GPG commands:" << std::endl; + out << " add-gpg-user USERID add the user with the given GPG user ID as a collaborator" << std::endl; + //out << " rm-gpg-user USERID revoke collaborator status from the given GPG user ID" << std::endl; + //out << " ls-gpg-users list the GPG key IDs of collaborators" << std::endl; + out << " unlock decrypt this repo using the in-repo GPG-encrypted key" << std::endl; + out << std::endl; + out << "Symmetric key commands:" << std::endl; + out << " export-key FILE export this repo's symmetric key to the given file" << std::endl; + out << " unlock KEYFILE decrypt this repo using the given symmetric key" << std::endl; + out << std::endl; + out << "Legacy commands:" << std::endl; + out << " init KEYFILE alias for 'unlock KEYFILE'" << std::endl; + out << " keygen KEYFILE generate a git-crypt key in the given file" << std::endl; + out << " migrate-key OLD NEW migrate the legacy key file OLD to the new format in NEW" << std::endl; + /* + out << std::endl; + out << "Plumbing commands (not to be used directly):" << std::endl; + out << " clean [LEGACY-KEYFILE]" << std::endl; + out << " smudge [LEGACY-KEYFILE]" << std::endl; + out << " diff [LEGACY-KEYFILE] FILE" << std::endl; + */ + out << std::endl; + out << "See 'git-crypt help COMMAND' for more information on a specific command." << std::endl; +} + +static void print_version (std::ostream& out) +{ + out << "git-crypt " << VERSION << std::endl; +} + +static bool help_for_command (const char* command, std::ostream& out) +{ + if (std::strcmp(command, "init") == 0) { + help_init(out); + } else if (std::strcmp(command, "unlock") == 0) { + help_unlock(out); + } else if (std::strcmp(command, "lock") == 0) { + help_lock(out); + } else if (std::strcmp(command, "add-gpg-user") == 0) { + help_add_gpg_user(out); + } else if (std::strcmp(command, "rm-gpg-user") == 0) { + help_rm_gpg_user(out); + } else if (std::strcmp(command, "ls-gpg-users") == 0) { + help_ls_gpg_users(out); + } else if (std::strcmp(command, "export-key") == 0) { + help_export_key(out); + } else if (std::strcmp(command, "keygen") == 0) { + help_keygen(out); + } else if (std::strcmp(command, "migrate-key") == 0) { + help_migrate_key(out); + } else if (std::strcmp(command, "refresh") == 0) { + help_refresh(out); + } else if (std::strcmp(command, "status") == 0) { + help_status(out); + } else { + return false; + } + return true; +} + +static int help (int argc, const char** argv) +{ + if (argc == 0) { + print_usage(std::cout); + } else { + if (!help_for_command(argv[0], std::cout)) { + std::clog << "Error: '" << argv[0] << "' is not a git-crypt command. See 'git-crypt help'." << std::endl; + return 1; + } + } + return 0; +} + +static int version (int argc, const char** argv) +{ + print_version(std::cout); + return 0; } int main (int argc, const char** argv) try { - // The following two lines are essential for achieving good performance: - std::ios_base::sync_with_stdio(false); - std::cin.tie(0); + argv0 = argv[0]; - std::cin.exceptions(std::ios_base::badbit); - std::cout.exceptions(std::ios_base::badbit); + /* + * General initialization + */ - if (argc < 3) { - print_usage(argv[0]); + init_std_streams(); + init_crypto(); + + /* + * Parse command line arguments + */ + int arg_index = 1; + while (arg_index < argc && argv[arg_index][0] == '-') { + if (std::strcmp(argv[arg_index], "--help") == 0) { + print_usage(std::clog); + return 0; + } else if (std::strcmp(argv[arg_index], "--version") == 0) { + print_version(std::clog); + return 0; + } else if (std::strcmp(argv[arg_index], "--") == 0) { + ++arg_index; + break; + } else { + std::clog << argv0 << ": " << argv[arg_index] << ": Unknown option" << std::endl; + print_usage(std::clog); + return 2; + } + } + + argc -= arg_index; + argv += arg_index; + + if (argc == 0) { + print_usage(std::clog); return 2; } - ERR_load_crypto_strings(); + /* + * Pass off to command handler + */ + const char* command = argv[0]; + --argc; + ++argv; - 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]); + try { + // Public commands: + if (std::strcmp(command, "help") == 0) { + return help(argc, argv); + } + if (std::strcmp(command, "version") == 0) { + return version(argc, argv); + } + if (std::strcmp(command, "init") == 0) { + return init(argc, argv); + } + if (std::strcmp(command, "unlock") == 0) { + return unlock(argc, argv); + } + if (std::strcmp(command, "lock") == 0) { + return lock(argc, argv); + } + if (std::strcmp(command, "add-gpg-user") == 0) { + return add_gpg_user(argc, argv); + } + if (std::strcmp(command, "rm-gpg-user") == 0) { + return rm_gpg_user(argc, argv); + } + if (std::strcmp(command, "ls-gpg-users") == 0) { + return ls_gpg_users(argc, argv); + } + if (std::strcmp(command, "export-key") == 0) { + return export_key(argc, argv); + } + if (std::strcmp(command, "keygen") == 0) { + return keygen(argc, argv); + } + if (std::strcmp(command, "migrate-key") == 0) { + return migrate_key(argc, argv); + } + if (std::strcmp(command, "refresh") == 0) { + return refresh(argc, argv); + } + if (std::strcmp(command, "status") == 0) { + return status(argc, argv); + } + // Plumbing commands (executed by git, not by user): + if (std::strcmp(command, "clean") == 0) { + return clean(argc, argv); + } + if (std::strcmp(command, "smudge") == 0) { + return smudge(argc, argv); + } + if (std::strcmp(command, "diff") == 0) { + return diff(argc, argv); + } + } catch (const Option_error& e) { + std::clog << "git-crypt: Error: " << e.option_name << ": " << e.message << std::endl; + help_for_command(command, std::clog); return 2; } - return 0; + std::clog << "Error: '" << command << "' is not a git-crypt command. See 'git-crypt help'." << std::endl; + return 2; + +} catch (const Error& e) { + std::cerr << "git-crypt: Error: " << e.message << std::endl; + return 1; +} catch (const Gpg_error& e) { + std::cerr << "git-crypt: GPG error: " << e.message << std::endl; + return 1; +} catch (const System_error& e) { + std::cerr << "git-crypt: System error: " << e.message() << std::endl; + return 1; +} catch (const Crypto_error& e) { + std::cerr << "git-crypt: Crypto error: " << e.where << ": " << e.message << std::endl; + return 1; +} catch (Key_file::Incompatible) { + std::cerr << "git-crypt: This repository contains a incompatible key file. Please upgrade git-crypt." << std::endl; + return 1; +} catch (Key_file::Malformed) { + std::cerr << "git-crypt: This repository contains a malformed key file. It may be corrupted." << std::endl; + return 1; } catch (const std::ios_base::failure& e) { std::cerr << "git-crypt: I/O error: " << e.what() << std::endl; return 1; diff --git a/git-crypt.hpp b/git-crypt.hpp new file mode 100644 index 0000000..78b8196 --- /dev/null +++ b/git-crypt.hpp @@ -0,0 +1,38 @@ +/* + * Copyright 2014 Andrew Ayer + * + * This file is part of git-crypt. + * + * git-crypt is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * git-crypt is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with git-crypt. If not, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify the Program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, the licensors of the Program + * grant you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#ifndef GIT_CRYPT_GIT_CRYPT_HPP +#define GIT_CRYPT_GIT_CRYPT_HPP + +#define VERSION "0.4.2" + +extern const char* argv0; // initialized in main() to argv[0] + +#endif diff --git a/gpg.cpp b/gpg.cpp new file mode 100644 index 0000000..4813b35 --- /dev/null +++ b/gpg.cpp @@ -0,0 +1,176 @@ +/* + * Copyright 2014 Andrew Ayer + * + * This file is part of git-crypt. + * + * git-crypt is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * git-crypt is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with git-crypt. If not, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify the Program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, the licensors of the Program + * grant you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#include "gpg.hpp" +#include "util.hpp" +#include + +static std::string gpg_nth_column (const std::string& line, unsigned int col) +{ + std::string::size_type pos = 0; + + for (unsigned int i = 0; i < col; ++i) { + pos = line.find_first_of(':', pos); + if (pos == std::string::npos) { + throw Gpg_error("Malformed output from gpg"); + } + pos = pos + 1; + } + + const std::string::size_type end_pos = line.find_first_of(':', pos); + + return end_pos != std::string::npos ? + line.substr(pos, end_pos - pos) : + line.substr(pos); +} + +// given a key fingerprint, return the last 8 nibbles +std::string gpg_shorten_fingerprint (const std::string& fingerprint) +{ + return fingerprint.size() == 40 ? fingerprint.substr(32) : fingerprint; +} + +// given a key fingerprint, return the key's UID (e.g. "John Smith ") +std::string gpg_get_uid (const std::string& fingerprint) +{ + // gpg --batch --with-colons --fixed-list-mode --list-keys 0x7A399B2DB06D039020CD1CE1D0F3702D61489532 + std::vector command; + command.push_back("gpg"); + command.push_back("--batch"); + command.push_back("--with-colons"); + command.push_back("--fixed-list-mode"); + command.push_back("--list-keys"); + command.push_back("0x" + fingerprint); + std::stringstream command_output; + if (!successful_exit(exec_command(command, command_output))) { + // This could happen if the keyring does not contain a public key with this fingerprint + return ""; + } + + while (command_output.peek() != -1) { + std::string line; + std::getline(command_output, line); + if (line.substr(0, 4) == "uid:") { + // uid:u::::1395975462::AB97D6E3E5D8789988CA55E5F77D9E7397D05229::John Smith : + // want the 9th column (counting from 0) + return gpg_nth_column(line, 9); + } + } + + return ""; +} + +// return a list of fingerprints of public keys matching the given search query (such as jsmith@example.com) +std::vector gpg_lookup_key (const std::string& query) +{ + std::vector fingerprints; + + // gpg --batch --with-colons --fingerprint --list-keys jsmith@example.com + std::vector command; + command.push_back("gpg"); + command.push_back("--batch"); + command.push_back("--with-colons"); + command.push_back("--fingerprint"); + command.push_back("--list-keys"); + command.push_back(query); + std::stringstream command_output; + if (successful_exit(exec_command(command, command_output))) { + while (command_output.peek() != -1) { + std::string line; + std::getline(command_output, line); + if (line.substr(0, 4) == "fpr:") { + // fpr:::::::::7A399B2DB06D039020CD1CE1D0F3702D61489532: + // want the 9th column (counting from 0) + fingerprints.push_back(gpg_nth_column(line, 9)); + } + } + } + + return fingerprints; +} + +std::vector gpg_list_secret_keys () +{ + // gpg --batch --with-colons --list-secret-keys --fingerprint + std::vector command; + command.push_back("gpg"); + command.push_back("--batch"); + command.push_back("--with-colons"); + command.push_back("--list-secret-keys"); + command.push_back("--fingerprint"); + std::stringstream command_output; + if (!successful_exit(exec_command(command, command_output))) { + throw Gpg_error("gpg --list-secret-keys failed"); + } + + std::vector secret_keys; + + while (command_output.peek() != -1) { + std::string line; + std::getline(command_output, line); + if (line.substr(0, 4) == "fpr:") { + // fpr:::::::::7A399B2DB06D039020CD1CE1D0F3702D61489532: + // want the 9th column (counting from 0) + secret_keys.push_back(gpg_nth_column(line, 9)); + } + } + + return secret_keys; +} + +void gpg_encrypt_to_file (const std::string& filename, const std::string& recipient_fingerprint, const char* p, size_t len) +{ + // gpg --batch -o FILENAME -r RECIPIENT -e + std::vector command; + command.push_back("gpg"); + command.push_back("--batch"); + command.push_back("-o"); + command.push_back(filename); + command.push_back("-r"); + command.push_back("0x" + recipient_fingerprint); + command.push_back("-e"); + if (!successful_exit(exec_command_with_input(command, p, len))) { + throw Gpg_error("Failed to encrypt"); + } +} + +void gpg_decrypt_from_file (const std::string& filename, std::ostream& output) +{ + // gpg -q -d FILENAME + std::vector command; + command.push_back("gpg"); + command.push_back("-q"); + command.push_back("-d"); + command.push_back(filename); + if (!successful_exit(exec_command(command, output))) { + throw Gpg_error("Failed to decrypt"); + } +} + diff --git a/gpg.hpp b/gpg.hpp new file mode 100644 index 0000000..cd55171 --- /dev/null +++ b/gpg.hpp @@ -0,0 +1,51 @@ +/* + * Copyright 2014 Andrew Ayer + * + * This file is part of git-crypt. + * + * git-crypt is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * git-crypt is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with git-crypt. If not, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify the Program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, the licensors of the Program + * grant you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#ifndef GIT_CRYPT_GPG_HPP +#define GIT_CRYPT_GPG_HPP + +#include +#include +#include + +struct Gpg_error { + std::string message; + + explicit Gpg_error (std::string m) : message(m) { } +}; + +std::string gpg_shorten_fingerprint (const std::string& fingerprint); +std::string gpg_get_uid (const std::string& fingerprint); +std::vector gpg_lookup_key (const std::string& query); +std::vector gpg_list_secret_keys (); +void gpg_encrypt_to_file (const std::string& filename, const std::string& recipient_fingerprint, const char* p, size_t len); +void gpg_decrypt_from_file (const std::string& filename, std::ostream&); + +#endif diff --git a/key.cpp b/key.cpp new file mode 100644 index 0000000..d8fa482 --- /dev/null +++ b/key.cpp @@ -0,0 +1,336 @@ +/* + * Copyright 2014 Andrew Ayer + * + * This file is part of git-crypt. + * + * git-crypt is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * git-crypt is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with git-crypt. If not, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify the Program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, the licensors of the Program + * grant you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#include "key.hpp" +#include "util.hpp" +#include "crypto.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Key_file::Entry::Entry () +{ + version = 0; + explicit_memset(aes_key, 0, AES_KEY_LEN); + explicit_memset(hmac_key, 0, HMAC_KEY_LEN); +} + +void Key_file::Entry::load (std::istream& in) +{ + while (true) { + uint32_t field_id; + if (!read_be32(in, field_id)) { + throw Malformed(); + } + if (field_id == KEY_FIELD_END) { + break; + } + uint32_t field_len; + if (!read_be32(in, field_len)) { + throw Malformed(); + } + + if (field_id == KEY_FIELD_VERSION) { + if (field_len != 4) { + throw Malformed(); + } + if (!read_be32(in, version)) { + throw Malformed(); + } + } else if (field_id == KEY_FIELD_AES_KEY) { + if (field_len != AES_KEY_LEN) { + throw Malformed(); + } + in.read(reinterpret_cast(aes_key), AES_KEY_LEN); + if (in.gcount() != AES_KEY_LEN) { + throw Malformed(); + } + } else if (field_id == KEY_FIELD_HMAC_KEY) { + if (field_len != HMAC_KEY_LEN) { + throw Malformed(); + } + in.read(reinterpret_cast(hmac_key), HMAC_KEY_LEN); + if (in.gcount() != HMAC_KEY_LEN) { + throw Malformed(); + } + } else if (field_id & 1) { // unknown critical field + throw Incompatible(); + } else { + // unknown non-critical field - safe to ignore + if (field_len > MAX_FIELD_LEN) { + throw Malformed(); + } + in.ignore(field_len); + if (in.gcount() != static_cast(field_len)) { + throw Malformed(); + } + } + } +} + +void Key_file::Entry::load_legacy (uint32_t arg_version, std::istream& in) +{ + version = arg_version; + + // First comes the AES key + in.read(reinterpret_cast(aes_key), AES_KEY_LEN); + if (in.gcount() != AES_KEY_LEN) { + throw Malformed(); + } + + // Then the HMAC key + in.read(reinterpret_cast(hmac_key), HMAC_KEY_LEN); + if (in.gcount() != HMAC_KEY_LEN) { + throw Malformed(); + } + + if (in.peek() != -1) { + // Trailing data is a good indication that we are not actually reading a + // legacy key file. (This is important to check since legacy key files + // did not have any sort of file header.) + throw Malformed(); + } +} + +void Key_file::Entry::store (std::ostream& out) const +{ + // Version + write_be32(out, KEY_FIELD_VERSION); + write_be32(out, 4); + write_be32(out, version); + + // AES key + write_be32(out, KEY_FIELD_AES_KEY); + write_be32(out, AES_KEY_LEN); + out.write(reinterpret_cast(aes_key), AES_KEY_LEN); + + // HMAC key + write_be32(out, KEY_FIELD_HMAC_KEY); + write_be32(out, HMAC_KEY_LEN); + out.write(reinterpret_cast(hmac_key), HMAC_KEY_LEN); + + // End + write_be32(out, KEY_FIELD_END); +} + +void Key_file::Entry::generate (uint32_t arg_version) +{ + version = arg_version; + random_bytes(aes_key, AES_KEY_LEN); + random_bytes(hmac_key, HMAC_KEY_LEN); +} + +const Key_file::Entry* Key_file::get_latest () const +{ + return is_filled() ? get(latest()) : 0; +} + +const Key_file::Entry* Key_file::get (uint32_t version) const +{ + Map::const_iterator it(entries.find(version)); + return it != entries.end() ? &it->second : 0; +} + +void Key_file::add (const Entry& entry) +{ + entries[entry.version] = entry; +} + + +void Key_file::load_legacy (std::istream& in) +{ + entries[0].load_legacy(0, in); +} + +void Key_file::load (std::istream& in) +{ + unsigned char preamble[16]; + in.read(reinterpret_cast(preamble), 16); + if (in.gcount() != 16) { + throw Malformed(); + } + if (std::memcmp(preamble, "\0GITCRYPTKEY", 12) != 0) { + throw Malformed(); + } + if (load_be32(preamble + 12) != FORMAT_VERSION) { + throw Incompatible(); + } + load_header(in); + while (in.peek() != -1) { + Entry entry; + entry.load(in); + add(entry); + } +} + +void Key_file::load_header (std::istream& in) +{ + while (true) { + uint32_t field_id; + if (!read_be32(in, field_id)) { + throw Malformed(); + } + if (field_id == HEADER_FIELD_END) { + break; + } + uint32_t field_len; + if (!read_be32(in, field_len)) { + throw Malformed(); + } + + if (field_id == HEADER_FIELD_KEY_NAME) { + if (field_len > KEY_NAME_MAX_LEN) { + throw Malformed(); + } + if (field_len == 0) { + // special case field_len==0 to avoid possible undefined behavior + // edge cases with an empty std::vector (particularly, &bytes[0]). + key_name.clear(); + } else { + std::vector bytes(field_len); + in.read(&bytes[0], field_len); + if (in.gcount() != static_cast(field_len)) { + throw Malformed(); + } + key_name.assign(&bytes[0], field_len); + } + if (!validate_key_name(key_name.c_str())) { + key_name.clear(); + throw Malformed(); + } + } else if (field_id & 1) { // unknown critical field + throw Incompatible(); + } else { + // unknown non-critical field - safe to ignore + if (field_len > MAX_FIELD_LEN) { + throw Malformed(); + } + in.ignore(field_len); + if (in.gcount() != static_cast(field_len)) { + throw Malformed(); + } + } + } +} + +void Key_file::store (std::ostream& out) const +{ + out.write("\0GITCRYPTKEY", 12); + write_be32(out, FORMAT_VERSION); + if (!key_name.empty()) { + write_be32(out, HEADER_FIELD_KEY_NAME); + write_be32(out, key_name.size()); + out.write(key_name.data(), key_name.size()); + } + write_be32(out, HEADER_FIELD_END); + for (Map::const_iterator it(entries.begin()); it != entries.end(); ++it) { + it->second.store(out); + } +} + +bool Key_file::load_from_file (const char* key_file_name) +{ + std::ifstream key_file_in(key_file_name, std::fstream::binary); + if (!key_file_in) { + return false; + } + load(key_file_in); + return true; +} + +bool Key_file::store_to_file (const char* key_file_name) const +{ + create_protected_file(key_file_name); + std::ofstream key_file_out(key_file_name, std::fstream::binary); + if (!key_file_out) { + return false; + } + store(key_file_out); + key_file_out.close(); + if (!key_file_out) { + return false; + } + return true; +} + +std::string Key_file::store_to_string () const +{ + std::ostringstream ss; + store(ss); + return ss.str(); +} + +void Key_file::generate () +{ + uint32_t version(is_empty() ? 0 : latest() + 1); + entries[version].generate(version); +} + +uint32_t Key_file::latest () const +{ + if (is_empty()) { + throw std::invalid_argument("Key_file::latest"); + } + return entries.begin()->first; +} + +bool validate_key_name (const char* key_name, std::string* reason) +{ + if (!*key_name) { + if (reason) { *reason = "Key name may not be empty"; } + return false; + } + + if (std::strcmp(key_name, "default") == 0) { + if (reason) { *reason = "`default' is not a legal key name"; } + return false; + } + // Need to be restrictive with key names because they're used as part of a Git filter name + size_t len = 0; + while (char c = *key_name++) { + if (!std::isalnum(c) && c != '-' && c != '_') { + if (reason) { *reason = "Key names may contain only A-Z, a-z, 0-9, '-', and '_'"; } + return false; + } + if (++len > KEY_NAME_MAX_LEN) { + if (reason) { *reason = "Key name is too long"; } + return false; + } + } + return true; +} + diff --git a/key.hpp b/key.hpp new file mode 100644 index 0000000..2695581 --- /dev/null +++ b/key.hpp @@ -0,0 +1,116 @@ +/* + * Copyright 2014 Andrew Ayer + * + * This file is part of git-crypt. + * + * git-crypt is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * git-crypt is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with git-crypt. If not, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify the Program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, the licensors of the Program + * grant you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#ifndef GIT_CRYPT_KEY_HPP +#define GIT_CRYPT_KEY_HPP + +#include +#include +#include +#include +#include + +enum { + HMAC_KEY_LEN = 64, + AES_KEY_LEN = 32 +}; + +struct Key_file { +public: + struct Entry { + uint32_t version; + unsigned char aes_key[AES_KEY_LEN]; + unsigned char hmac_key[HMAC_KEY_LEN]; + + Entry (); + + void load (std::istream&); + void load_legacy (uint32_t version, std::istream&); + void store (std::ostream&) const; + void generate (uint32_t version); + }; + + struct Malformed { }; // exception class + struct Incompatible { }; // exception class + + const Entry* get_latest () const; + + const Entry* get (uint32_t version) const; + void add (const Entry&); + + void load_legacy (std::istream&); + void load (std::istream&); + void store (std::ostream&) const; + + bool load_from_file (const char* filename); + bool store_to_file (const char* filename) const; + + std::string store_to_string () const; + + void generate (); + + bool is_empty () const { return entries.empty(); } + bool is_filled () const { return !is_empty(); } + + uint32_t latest () const; + + 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(); } +private: + typedef std::map > Map; + enum { FORMAT_VERSION = 2 }; + + Map entries; + std::string key_name; + + void load_header (std::istream&); + + enum { + HEADER_FIELD_END = 0, + HEADER_FIELD_KEY_NAME = 1 + }; + enum { + KEY_FIELD_END = 0, + KEY_FIELD_VERSION = 1, + KEY_FIELD_AES_KEY = 3, + KEY_FIELD_HMAC_KEY = 5 + }; + enum { + MAX_FIELD_LEN = 1<<20 + }; +}; + +enum { + KEY_NAME_MAX_LEN = 128 +}; + +bool validate_key_name (const char* key_name, std::string* reason =0); + +#endif diff --git a/parse_options.cpp b/parse_options.cpp new file mode 100644 index 0000000..51b51f7 --- /dev/null +++ b/parse_options.cpp @@ -0,0 +1,118 @@ +/* + * Copyright 2014 Andrew Ayer + * + * This file is part of git-crypt. + * + * git-crypt is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * git-crypt is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with git-crypt. If not, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify the Program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, the licensors of the Program + * grant you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#include "parse_options.hpp" +#include + + +static const Option_def* find_option (const Options_list& options, const std::string& name) +{ + for (Options_list::const_iterator opt(options.begin()); opt != options.end(); ++opt) { + if (opt->name == name) { + return &*opt; + } + } + return 0; +} + +int parse_options (const Options_list& options, int argc, const char** argv) +{ + int argi = 0; + + while (argi < argc && argv[argi][0] == '-') { + if (std::strcmp(argv[argi], "--") == 0) { + ++argi; + break; + } else if (std::strncmp(argv[argi], "--", 2) == 0) { + std::string option_name; + const char* option_value = 0; + if (const char* eq = std::strchr(argv[argi], '=')) { + option_name.assign(argv[argi], eq); + option_value = eq + 1; + } else { + option_name = argv[argi]; + } + ++argi; + + const Option_def* opt(find_option(options, option_name)); + if (!opt) { + throw Option_error(option_name, "Invalid option"); + } + + if (opt->is_set) { + *opt->is_set = true; + } + if (opt->value) { + if (option_value) { + *opt->value = option_value; + } else { + if (argi >= argc) { + throw Option_error(option_name, "Option requires a value"); + } + *opt->value = argv[argi]; + ++argi; + } + } else { + if (option_value) { + throw Option_error(option_name, "Option takes no value"); + } + } + } else { + const char* arg = argv[argi] + 1; + ++argi; + while (*arg) { + std::string option_name("-"); + option_name.push_back(*arg); + ++arg; + + const Option_def* opt(find_option(options, option_name)); + if (!opt) { + throw Option_error(option_name, "Invalid option"); + } + if (opt->is_set) { + *opt->is_set = true; + } + if (opt->value) { + if (*arg) { + *opt->value = arg; + } else { + if (argi >= argc) { + throw Option_error(option_name, "Option requires a value"); + } + *opt->value = argv[argi]; + ++argi; + } + break; + } + } + } + } + return argi; +} diff --git a/parse_options.hpp b/parse_options.hpp new file mode 100644 index 0000000..c0580f0 --- /dev/null +++ b/parse_options.hpp @@ -0,0 +1,60 @@ +/* + * Copyright 2014 Andrew Ayer + * + * This file is part of git-crypt. + * + * git-crypt is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * git-crypt is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with git-crypt. If not, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify the Program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, the licensors of the Program + * grant you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#ifndef PARSE_OPTIONS_HPP +#define PARSE_OPTIONS_HPP + +#include +#include + +struct Option_def { + std::string name; + bool* is_set; + const char** value; + + Option_def () : is_set(0), value(0) { } + Option_def (const std::string& arg_name, bool* arg_is_set) + : name(arg_name), is_set(arg_is_set), value(0) { } + Option_def (const std::string& arg_name, const char** arg_value) + : name(arg_name), is_set(0), value(arg_value) { } +}; + +typedef std::vector Options_list; + +int parse_options (const Options_list& options, int argc, const char** argv); + +struct Option_error { + std::string option_name; + std::string message; + + Option_error (const std::string& n, const std::string& m) : option_name(n), message(m) { } +}; + +#endif diff --git a/util-unix.cpp b/util-unix.cpp new file mode 100644 index 0000000..1cebf3f --- /dev/null +++ b/util-unix.cpp @@ -0,0 +1,334 @@ +/* + * Copyright 2012, 2014 Andrew Ayer + * + * This file is part of git-crypt. + * + * git-crypt is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * git-crypt is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with git-crypt. If not, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify the Program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, the licensors of the Program + * grant you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +std::string System_error::message () const +{ + std::string mesg(action); + if (!target.empty()) { + mesg += ": "; + mesg += target; + } + if (error) { + mesg += ": "; + mesg += strerror(error); + } + return mesg; +} + +void temp_fstream::open (std::ios_base::openmode mode) +{ + close(); + + const char* tmpdir = getenv("TMPDIR"); + size_t tmpdir_len = tmpdir ? std::strlen(tmpdir) : 0; + if (tmpdir_len == 0 || tmpdir_len > 4096) { + // no $TMPDIR or it's excessively long => fall back to /tmp + tmpdir = "/tmp"; + tmpdir_len = 4; + } + std::vector path_buffer(tmpdir_len + 18); + char* path = &path_buffer[0]; + std::strcpy(path, tmpdir); + std::strcpy(path + tmpdir_len, "/git-crypt.XXXXXX"); + mode_t old_umask = umask(0077); + int fd = mkstemp(path); + if (fd == -1) { + int mkstemp_errno = errno; + umask(old_umask); + throw System_error("mkstemp", "", mkstemp_errno); + } + umask(old_umask); + std::fstream::open(path, mode); + if (!std::fstream::is_open()) { + unlink(path); + ::close(fd); + throw System_error("std::fstream::open", path, 0); + } + unlink(path); + ::close(fd); +} + +void temp_fstream::close () +{ + if (std::fstream::is_open()) { + std::fstream::close(); + } +} + +void mkdir_parent (const std::string& path) +{ + std::string::size_type slash(path.find('/', 1)); + while (slash != std::string::npos) { + std::string prefix(path.substr(0, slash)); + struct stat status; + if (stat(prefix.c_str(), &status) == 0) { + // already exists - make sure it's a directory + if (!S_ISDIR(status.st_mode)) { + throw System_error("mkdir_parent", prefix, ENOTDIR); + } + } else { + if (errno != ENOENT) { + throw System_error("mkdir_parent", prefix, errno); + } + // doesn't exist - mkdir it + if (mkdir(prefix.c_str(), 0777) == -1) { + throw System_error("mkdir", prefix, errno); + } + } + + slash = path.find('/', slash + 1); + } +} + +static std::string readlink (const char* pathname) +{ + std::vector buffer(64); + ssize_t len; + + while ((len = ::readlink(pathname, &buffer[0], buffer.size())) == static_cast(buffer.size())) { + // buffer may have been truncated - grow and try again + buffer.resize(buffer.size() * 2); + } + if (len == -1) { + throw System_error("readlink", pathname, errno); + } + + return std::string(buffer.begin(), buffer.begin() + len); +} + +std::string our_exe_path () +{ + try { + return readlink("/proc/self/exe"); + } catch (const System_error&) { + if (argv0[0] == '/') { + // argv[0] starts with / => it's an absolute path + return argv0; + } else if (std::strchr(argv0, '/')) { + // argv[0] contains / => it a relative path that should be resolved + char* resolved_path_p = realpath(argv0, NULL); + std::string resolved_path(resolved_path_p); + free(resolved_path_p); + return resolved_path; + } else { + // argv[0] is just a bare filename => not much we can do + return argv0; + } + } +} + +static int execvp (const std::string& file, const std::vector& args) +{ + std::vector args_c_str; + args_c_str.reserve(args.size()); + for (std::vector::const_iterator arg(args.begin()); arg != args.end(); ++arg) { + args_c_str.push_back(arg->c_str()); + } + args_c_str.push_back(NULL); + return execvp(file.c_str(), const_cast(&args_c_str[0])); +} + +int exec_command (const std::vector& command) +{ + pid_t child = fork(); + if (child == -1) { + throw System_error("fork", "", errno); + } + if (child == 0) { + execvp(command[0], command); + perror(command[0].c_str()); + _exit(-1); + } + int status = 0; + if (waitpid(child, &status, 0) == -1) { + throw System_error("waitpid", "", errno); + } + return status; +} + +int exec_command (const std::vector& command, std::ostream& output) +{ + int pipefd[2]; + if (pipe(pipefd) == -1) { + throw System_error("pipe", "", errno); + } + pid_t child = fork(); + if (child == -1) { + int fork_errno = errno; + close(pipefd[0]); + close(pipefd[1]); + throw System_error("fork", "", fork_errno); + } + if (child == 0) { + close(pipefd[0]); + if (pipefd[1] != 1) { + dup2(pipefd[1], 1); + close(pipefd[1]); + } + execvp(command[0], command); + perror(command[0].c_str()); + _exit(-1); + } + close(pipefd[1]); + char buffer[1024]; + ssize_t bytes_read; + while ((bytes_read = read(pipefd[0], buffer, sizeof(buffer))) > 0) { + output.write(buffer, bytes_read); + } + if (bytes_read == -1) { + int read_errno = errno; + close(pipefd[0]); + throw System_error("read", "", read_errno); + } + close(pipefd[0]); + int status = 0; + if (waitpid(child, &status, 0) == -1) { + throw System_error("waitpid", "", errno); + } + return status; +} + +int exec_command_with_input (const std::vector& command, const char* p, size_t len) +{ + int pipefd[2]; + if (pipe(pipefd) == -1) { + throw System_error("pipe", "", errno); + } + pid_t child = fork(); + if (child == -1) { + int fork_errno = errno; + close(pipefd[0]); + close(pipefd[1]); + throw System_error("fork", "", fork_errno); + } + if (child == 0) { + close(pipefd[1]); + if (pipefd[0] != 0) { + dup2(pipefd[0], 0); + close(pipefd[0]); + } + execvp(command[0], command); + perror(command[0].c_str()); + _exit(-1); + } + close(pipefd[0]); + while (len > 0) { + ssize_t bytes_written = write(pipefd[1], p, len); + if (bytes_written == -1) { + int write_errno = errno; + close(pipefd[1]); + throw System_error("write", "", write_errno); + } + p += bytes_written; + len -= bytes_written; + } + close(pipefd[1]); + int status = 0; + if (waitpid(child, &status, 0) == -1) { + throw System_error("waitpid", "", errno); + } + return status; +} + +bool successful_exit (int status) +{ + return status != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0; +} + +void touch_file (const std::string& filename) +{ + if (utimes(filename.c_str(), NULL) == -1) { + throw System_error("utimes", filename, errno); + } +} + +void remove_file (const std::string& filename) +{ + if (unlink(filename.c_str()) == -1) { + throw System_error("unlink", filename, errno); + } +} + +static void init_std_streams_platform () +{ +} + +void create_protected_file (const char* path) +{ + int fd = open(path, O_WRONLY | O_CREAT, 0600); + if (fd == -1) { + throw System_error("open", path, errno); + } + close(fd); +} + +int util_rename (const char* from, const char* to) +{ + return rename(from, to); +} + +static int dirfilter (const struct dirent* ent) +{ + // filter out . and .. + return std::strcmp(ent->d_name, ".") != 0 && std::strcmp(ent->d_name, "..") != 0; +} + +std::vector get_directory_contents (const char* path) +{ + struct dirent** namelist; + int n = scandir(path, &namelist, dirfilter, alphasort); + if (n == -1) { + throw System_error("scandir", path, errno); + } + std::vector contents(n); + for (int i = 0; i < n; ++i) { + contents[i] = namelist[i]->d_name; + free(namelist[i]); + } + free(namelist); + + return contents; +} diff --git a/util-win32.cpp b/util-win32.cpp new file mode 100644 index 0000000..21576c7 --- /dev/null +++ b/util-win32.cpp @@ -0,0 +1,393 @@ +/* + * Copyright 2014 Andrew Ayer + * + * This file is part of git-crypt. + * + * git-crypt is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * git-crypt is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with git-crypt. If not, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify the Program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, the licensors of the Program + * grant you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#include +#include +#include +#include +#include +#include + +std::string System_error::message () const +{ + std::string mesg(action); + if (!target.empty()) { + mesg += ": "; + mesg += target; + } + if (error) { + LPTSTR error_message; + FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + error, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&error_message), + 0, + NULL); + mesg += error_message; + LocalFree(error_message); + } + return mesg; +} + +void temp_fstream::open (std::ios_base::openmode mode) +{ + close(); + + char tmpdir[MAX_PATH + 1]; + + DWORD ret = GetTempPath(sizeof(tmpdir), tmpdir); + if (ret == 0) { + throw System_error("GetTempPath", "", GetLastError()); + } else if (ret > sizeof(tmpdir) - 1) { + throw System_error("GetTempPath", "", ERROR_BUFFER_OVERFLOW); + } + + char tmpfilename[MAX_PATH + 1]; + if (GetTempFileName(tmpdir, TEXT("git-crypt"), 0, tmpfilename) == 0) { + throw System_error("GetTempFileName", "", GetLastError()); + } + + filename = tmpfilename; + + std::fstream::open(filename.c_str(), mode); + if (!std::fstream::is_open()) { + DeleteFile(filename.c_str()); + throw System_error("std::fstream::open", filename, 0); + } +} + +void temp_fstream::close () +{ + if (std::fstream::is_open()) { + std::fstream::close(); + DeleteFile(filename.c_str()); + } +} + +void mkdir_parent (const std::string& path) +{ + std::string::size_type slash(path.find('/', 1)); + while (slash != std::string::npos) { + std::string prefix(path.substr(0, slash)); + if (GetFileAttributes(prefix.c_str()) == INVALID_FILE_ATTRIBUTES) { + // prefix does not exist, so try to create it + if (!CreateDirectory(prefix.c_str(), NULL)) { + throw System_error("CreateDirectory", prefix, GetLastError()); + } + } + + slash = path.find('/', slash + 1); + } +} + +std::string our_exe_path () +{ + std::vector buffer(128); + size_t len; + + while ((len = GetModuleFileNameA(NULL, &buffer[0], buffer.size())) == buffer.size()) { + // buffer may have been truncated - grow and try again + buffer.resize(buffer.size() * 2); + } + if (len == 0) { + throw System_error("GetModuleFileNameA", "", GetLastError()); + } + + return std::string(buffer.begin(), buffer.begin() + len); +} + +static void escape_cmdline_argument (std::string& cmdline, const std::string& arg) +{ + // For an explanation of Win32's arcane argument quoting rules, see: + // http://msdn.microsoft.com/en-us/library/17w5ykft%28v=vs.85%29.aspx + // http://msdn.microsoft.com/en-us/library/bb776391%28v=vs.85%29.aspx + // http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx + // http://blogs.msdn.com/b/oldnewthing/archive/2010/09/17/10063629.aspx + cmdline.push_back('"'); + + std::string::const_iterator p(arg.begin()); + while (p != arg.end()) { + if (*p == '"') { + cmdline.push_back('\\'); + cmdline.push_back('"'); + ++p; + } else if (*p == '\\') { + unsigned int num_backslashes = 0; + while (p != arg.end() && *p == '\\') { + ++num_backslashes; + ++p; + } + if (p == arg.end() || *p == '"') { + // Backslashes need to be escaped + num_backslashes *= 2; + } + while (num_backslashes--) { + cmdline.push_back('\\'); + } + } else { + cmdline.push_back(*p++); + } + } + + cmdline.push_back('"'); +} + +static std::string format_cmdline (const std::vector& command) +{ + std::string cmdline; + for (std::vector::const_iterator arg(command.begin()); arg != command.end(); ++arg) { + if (arg != command.begin()) { + cmdline.push_back(' '); + } + escape_cmdline_argument(cmdline, *arg); + } + return cmdline; +} + +static int wait_for_child (HANDLE child_handle) +{ + if (WaitForSingleObject(child_handle, INFINITE) == WAIT_FAILED) { + throw System_error("WaitForSingleObject", "", GetLastError()); + } + + DWORD exit_code; + if (!GetExitCodeProcess(child_handle, &exit_code)) { + throw System_error("GetExitCodeProcess", "", GetLastError()); + } + + return exit_code; +} + +static HANDLE spawn_command (const std::vector& command, HANDLE stdin_handle, HANDLE stdout_handle, HANDLE stderr_handle) +{ + PROCESS_INFORMATION proc_info; + ZeroMemory(&proc_info, sizeof(proc_info)); + + STARTUPINFO start_info; + ZeroMemory(&start_info, sizeof(start_info)); + + start_info.cb = sizeof(STARTUPINFO); + start_info.hStdInput = stdin_handle ? stdin_handle : GetStdHandle(STD_INPUT_HANDLE); + start_info.hStdOutput = stdout_handle ? stdout_handle : GetStdHandle(STD_OUTPUT_HANDLE); + start_info.hStdError = stderr_handle ? stderr_handle : GetStdHandle(STD_ERROR_HANDLE); + start_info.dwFlags |= STARTF_USESTDHANDLES; + + std::string cmdline(format_cmdline(command)); + + if (!CreateProcessA(NULL, // application name (NULL to use command line) + const_cast(cmdline.c_str()), + NULL, // process security attributes + NULL, // primary thread security attributes + TRUE, // handles are inherited + 0, // creation flags + NULL, // use parent's environment + NULL, // use parent's current directory + &start_info, + &proc_info)) { + throw System_error("CreateProcess", cmdline, GetLastError()); + } + + CloseHandle(proc_info.hThread); + + return proc_info.hProcess; +} + +int exec_command (const std::vector& command) +{ + HANDLE child_handle = spawn_command(command, NULL, NULL, NULL); + int exit_code = wait_for_child(child_handle); + CloseHandle(child_handle); + return exit_code; +} + +int exec_command (const std::vector& command, std::ostream& output) +{ + HANDLE stdout_pipe_reader = NULL; + HANDLE stdout_pipe_writer = NULL; + SECURITY_ATTRIBUTES sec_attr; + + // Set the bInheritHandle flag so pipe handles are inherited. + sec_attr.nLength = sizeof(SECURITY_ATTRIBUTES); + sec_attr.bInheritHandle = TRUE; + sec_attr.lpSecurityDescriptor = NULL; + + // Create a pipe for the child process's STDOUT. + if (!CreatePipe(&stdout_pipe_reader, &stdout_pipe_writer, &sec_attr, 0)) { + throw System_error("CreatePipe", "", GetLastError()); + } + + // Ensure the read handle to the pipe for STDOUT is not inherited. + if (!SetHandleInformation(stdout_pipe_reader, HANDLE_FLAG_INHERIT, 0)) { + throw System_error("SetHandleInformation", "", GetLastError()); + } + + HANDLE child_handle = spawn_command(command, NULL, stdout_pipe_writer, NULL); + CloseHandle(stdout_pipe_writer); + + // Read from stdout_pipe_reader. + // Note that ReadFile on a pipe may return with bytes_read==0 if the other + // end of the pipe writes zero bytes, so don't break out of the read loop + // when this happens. When the other end of the pipe closes, ReadFile + // fails with ERROR_BROKEN_PIPE. + char buffer[1024]; + DWORD bytes_read; + while (ReadFile(stdout_pipe_reader, buffer, sizeof(buffer), &bytes_read, NULL)) { + output.write(buffer, bytes_read); + } + const DWORD read_error = GetLastError(); + if (read_error != ERROR_BROKEN_PIPE) { + throw System_error("ReadFile", "", read_error); + } + + CloseHandle(stdout_pipe_reader); + + int exit_code = wait_for_child(child_handle); + CloseHandle(child_handle); + return exit_code; +} + +int exec_command_with_input (const std::vector& command, const char* p, size_t len) +{ + HANDLE stdin_pipe_reader = NULL; + HANDLE stdin_pipe_writer = NULL; + SECURITY_ATTRIBUTES sec_attr; + + // Set the bInheritHandle flag so pipe handles are inherited. + sec_attr.nLength = sizeof(SECURITY_ATTRIBUTES); + sec_attr.bInheritHandle = TRUE; + sec_attr.lpSecurityDescriptor = NULL; + + // Create a pipe for the child process's STDIN. + if (!CreatePipe(&stdin_pipe_reader, &stdin_pipe_writer, &sec_attr, 0)) { + throw System_error("CreatePipe", "", GetLastError()); + } + + // Ensure the write handle to the pipe for STDIN is not inherited. + if (!SetHandleInformation(stdin_pipe_writer, HANDLE_FLAG_INHERIT, 0)) { + throw System_error("SetHandleInformation", "", GetLastError()); + } + + HANDLE child_handle = spawn_command(command, stdin_pipe_reader, NULL, NULL); + CloseHandle(stdin_pipe_reader); + + // Write to stdin_pipe_writer. + while (len > 0) { + DWORD bytes_written; + if (!WriteFile(stdin_pipe_writer, p, len, &bytes_written, NULL)) { + throw System_error("WriteFile", "", GetLastError()); + } + p += bytes_written; + len -= bytes_written; + } + + CloseHandle(stdin_pipe_writer); + + int exit_code = wait_for_child(child_handle); + CloseHandle(child_handle); + return exit_code; +} + +bool successful_exit (int status) +{ + return status == 0; +} + +void touch_file (const std::string& filename) +{ + HANDLE fh = CreateFileA(filename.c_str(), FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); + if (fh == INVALID_HANDLE_VALUE) { + throw System_error("CreateFileA", filename, GetLastError()); + } + SYSTEMTIME system_time; + GetSystemTime(&system_time); + FILETIME file_time; + SystemTimeToFileTime(&system_time, &file_time); + + if (!SetFileTime(fh, NULL, NULL, &file_time)) { + DWORD error = GetLastError(); + CloseHandle(fh); + throw System_error("SetFileTime", filename, error); + } + CloseHandle(fh); +} + +void remove_file (const std::string& filename) +{ + if (!DeleteFileA(filename.c_str())) { + throw System_error("DeleteFileA", filename, GetLastError()); + } +} + +static void init_std_streams_platform () +{ + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); +} + +void create_protected_file (const char* path) // TODO +{ +} + +int util_rename (const char* from, const char* to) +{ + // On Windows OS, it is necessary to ensure target file doesn't exist + unlink(to); + return rename(from, to); +} + +std::vector get_directory_contents (const char* path) +{ + std::vector filenames; + std::string patt(path); + if (!patt.empty() && patt[patt.size() - 1] != '/' && patt[patt.size() - 1] != '\\') { + patt.push_back('\\'); + } + patt.push_back('*'); + + WIN32_FIND_DATAA ffd; + HANDLE h = FindFirstFileA(patt.c_str(), &ffd); + if (h == INVALID_HANDLE_VALUE) { + throw System_error("FindFirstFileA", patt, GetLastError()); + } + do { + if (std::strcmp(ffd.cFileName, ".") != 0 && std::strcmp(ffd.cFileName, "..") != 0) { + filenames.push_back(ffd.cFileName); + } + } while (FindNextFileA(h, &ffd) != 0); + + DWORD err = GetLastError(); + if (err != ERROR_NO_MORE_FILES) { + throw System_error("FileNextFileA", patt, err); + } + FindClose(h); + return filenames; +} diff --git a/util.cpp b/util.cpp index 575d616..2da0622 100644 --- a/util.cpp +++ b/util.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -28,89 +28,10 @@ * as that of the covered work. */ +#include "git-crypt.hpp" #include "util.hpp" #include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -int exec_command (const char* command, std::ostream& 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.write(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; -} - -void open_tempfile (std::fstream& file, std::ios_base::openmode mode) -{ - const char* tmpdir = getenv("TMPDIR"); - size_t tmpdir_len; - if (tmpdir) { - tmpdir_len = strlen(tmpdir); - } else { - tmpdir = "/tmp"; - tmpdir_len = 4; - } - char* path = new char[tmpdir_len + 18]; - strcpy(path, tmpdir); - strcpy(path + tmpdir_len, "/git-crypt.XXXXXX"); - mode_t old_umask = umask(0077); - int fd = mkstemp(path); - if (fd == -1) { - perror("mkstemp"); - std::exit(9); - } - umask(old_umask); - file.open(path, mode); - if (!file.is_open()) { - perror("open"); - unlink(path); - std::exit(9); - } - unlink(path); - close(fd); - delete[] path; -} +#include std::string escape_shell_arg (const std::string& str) { @@ -126,3 +47,84 @@ std::string escape_shell_arg (const std::string& str) return new_str; } +uint32_t load_be32 (const unsigned char* p) +{ + return (static_cast(p[3]) << 0) | + (static_cast(p[2]) << 8) | + (static_cast(p[1]) << 16) | + (static_cast(p[0]) << 24); +} + +void store_be32 (unsigned char* p, uint32_t i) +{ + p[3] = i; i >>= 8; + p[2] = i; i >>= 8; + p[1] = i; i >>= 8; + p[0] = i; +} + +bool read_be32 (std::istream& in, uint32_t& i) +{ + unsigned char buffer[4]; + in.read(reinterpret_cast(buffer), 4); + if (in.gcount() != 4) { + return false; + } + i = load_be32(buffer); + return true; +} + +void write_be32 (std::ostream& out, uint32_t i) +{ + unsigned char buffer[4]; + store_be32(buffer, i); + out.write(reinterpret_cast(buffer), 4); +} + +void* explicit_memset (void* s, int c, std::size_t n) +{ + volatile unsigned char* p = reinterpret_cast(s); + + while (n--) { + *p++ = c; + } + + 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 () +{ + // The following two lines are essential for achieving good performance: + std::ios_base::sync_with_stdio(false); + std::cin.tie(0); + + std::cin.exceptions(std::ios_base::badbit); + std::cout.exceptions(std::ios_base::badbit); + + init_std_streams_platform(); +} + +#ifdef _WIN32 +#include "util-win32.cpp" +#else +#include "util-unix.cpp" +#endif diff --git a/util.hpp b/util.hpp index aa76982..8b5bc33 100644 --- a/util.hpp +++ b/util.hpp @@ -1,5 +1,5 @@ /* - * Copyright 2012 Andrew Ayer + * Copyright 2012, 2014 Andrew Ayer * * This file is part of git-crypt. * @@ -28,17 +28,55 @@ * as that of the covered work. */ -#ifndef _UTIL_H -#define _UTIL_H +#ifndef GIT_CRYPT_UTIL_HPP +#define GIT_CRYPT_UTIL_HPP #include #include #include +#include +#include +#include +#include -int exec_command (const char* command, std::ostream& output); -std::string resolve_path (const char* path); -void open_tempfile (std::fstream&, std::ios_base::openmode); +struct System_error { + std::string action; + std::string target; + int error; + + System_error (const std::string& a, const std::string& t, int e) : action(a), target(t), error(e) { } + + std::string message () const; +}; + +class temp_fstream : public std::fstream { + std::string filename; +public: + ~temp_fstream () { close(); } + + void open (std::ios_base::openmode); + void close (); +}; + +void mkdir_parent (const std::string& path); // Create parent directories of path, __but not path itself__ +std::string our_exe_path (); +int exec_command (const std::vector&); +int exec_command (const std::vector&, std::ostream& output); +int exec_command_with_input (const std::vector&, const char* p, size_t len); +bool successful_exit (int status); +void touch_file (const std::string&); +void remove_file (const std::string&); std::string escape_shell_arg (const std::string&); +uint32_t load_be32 (const unsigned char*); +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 (); +void create_protected_file (const char* path); // create empty file accessible only by current user +int util_rename (const char*, const char*); +std::vector get_directory_contents (const char* path); #endif