Files
git-crypt/commands.cpp
Andrew Ayer 6e43b2a1cd New exec_command() that takes command as array instead of string
This abstracts away the details of argument quoting, which differs
between Unix and Windows.

Also replace all uses of the system() library call with exec_command().
Although system() exists on Windows, it executes the command via cmd.exe,
which has ridiculous escaping rules.
2014-06-12 21:23:04 -07:00

788 lines
25 KiB
C++

/*
* 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 <http://www.gnu.org/licenses/>.
*
* 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 "commands.hpp"
#include "crypto.hpp"
#include "util.hpp"
#include "key.hpp"
#include "gpg.hpp"
#include <unistd.h>
#include <stdint.h>
#include <algorithm>
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <cstddef>
#include <cstring>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <vector>
static void git_config (const std::string& name, const std::string& value)
{
std::vector<std::string> 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 configure_git_filters ()
{
std::string escaped_git_crypt_path(escape_shell_arg(our_exe_path()));
git_config("filter.git-crypt.smudge", escaped_git_crypt_path + " smudge");
git_config("filter.git-crypt.clean", escaped_git_crypt_path + " clean");
git_config("diff.git-crypt.textconv", escaped_git_crypt_path + " diff");
}
static std::string get_internal_key_path ()
{
// git rev-parse --git-dir
std::vector<std::string> 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/key";
return path;
}
static std::string get_repo_keys_path ()
{
// git rev-parse --show-toplevel
std::vector<std::string> 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/keys";
return path;
}
static std::string get_path_to_top ()
{
// git rev-parse --show-cdup
std::vector<std::string> 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<std::string> 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?");
}
}
static bool check_if_head_exists ()
{
// git rev-parse HEAD
std::vector<std::string> command;
command.push_back("git");
command.push_back("rev-parse");
command.push_back("HEAD");
std::stringstream output;
return successful_exit(exec_command(command, output));
}
static void load_key (Key_file& key_file, 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 {
std::ifstream key_file_in(get_internal_key_path().c_str(), std::fstream::binary);
if (!key_file_in) {
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, uint32_t key_version, const std::vector<std::string>& secret_keys, const std::string& keys_path)
{
for (std::vector<std::string>::const_iterator seckey(secret_keys.begin()); seckey != secret_keys.end(); ++seckey) {
std::ostringstream path_builder;
path_builder << keys_path << '/' << key_version << '/' << *seckey;
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");
}
key_file.add(key_version, *this_version_entry);
return true;
}
}
return false;
}
static void encrypt_repo_key (uint32_t key_version, const Key_file::Entry& key, const std::vector<std::string>& collab_keys, const std::string& keys_path, std::vector<std::string>* new_files)
{
std::string key_file_data;
{
Key_file this_version_key_file;
this_version_key_file.add(key_version, key);
key_file_data = this_version_key_file.store_to_string();
}
for (std::vector<std::string>::const_iterator collab(collab_keys.begin()); collab != collab_keys.end(); ++collab) {
std::ostringstream path_builder;
path_builder << keys_path << '/' << key_version << '/' << *collab;
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);
}
}
// Encrypt contents of stdin and write to stdout
int clean (int argc, char** argv)
{
const char* legacy_key_path = 0;
if (argc == 0) {
} else if (argc == 1) {
legacy_key_path = argv[0];
} else {
std::clog << "Usage: git-crypt smudge" << std::endl;
return 2;
}
Key_file key_file;
load_key(key_file, 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(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];
while (std::cin && file_size < Aes_ctr_encryptor::MAX_CRYPT_BYTES) {
std::cin.read(buffer, sizeof(buffer));
size_t bytes_read = std::cin.gcount();
hmac.add(reinterpret_cast<unsigned char*>(buffer), bytes_read);
file_size += bytes_read;
if (file_size <= 8388608) {
file_contents.append(buffer, bytes_read);
} else {
if (!temp_file.is_open()) {
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 >= 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
// hasn't. CTR mode with a synthetic IV is provably semantically secure
// under deterministic CPA as long as the synthetic IV is derived from a
// secure PRF applied to the message. Since HMAC-SHA1 is a secure PRF, this
// encryption scheme is semantically secure under deterministic CPA.
//
// Informally, consider that if a file changes just a tiny bit, the IV will
// be completely different, resulting in a completely different ciphertext
// that leaks no information about the similarities of the plaintexts. Also,
// since we're using the output from a secure hash function plus a counter
// as the input to our block cipher, we should never have a situation where
// two different plaintext blocks get encrypted with the same CTR value. A
// nonce will be reused only if the entire file is the same, which leaks no
// information except that the files are the same.
//
// To prevent an attacker from building a dictionary of hash values and then
// looking up the nonce (which must be stored in the clear to allow for
// decryption), we use an HMAC as opposed to a straight hash.
// 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<char*>(digest), Aes_ctr_encryptor::NONCE_LEN); // ...includes the nonce
// Now encrypt the file and write to stdout
Aes_ctr_encryptor aes(key->aes_key, digest);
// First read from the in-memory copy
const unsigned char* file_data = reinterpret_cast<const unsigned char*>(file_contents.data());
size_t file_data_len = file_contents.size();
while (file_data_len > 0) {
size_t buffer_len = std::min(sizeof(buffer), file_data_len);
aes.process(file_data, reinterpret_cast<unsigned char*>(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.peek() != -1) {
temp_file.read(buffer, sizeof(buffer));
size_t buffer_len = temp_file.gcount();
aes.process(reinterpret_cast<unsigned char*>(buffer),
reinterpret_cast<unsigned char*>(buffer),
buffer_len);
std::cout.write(buffer, buffer_len);
}
}
return 0;
}
// Decrypt contents of stdin and write to stdout
int smudge (int argc, char** argv)
{
const char* legacy_key_path = 0;
if (argc == 0) {
} else if (argc == 1) {
legacy_key_path = argv[0];
} else {
std::clog << "Usage: git-crypt smudge" << std::endl;
return 2;
}
Key_file key_file;
load_key(key_file, legacy_key_path);
// Read the header to get the nonce and make sure it's actually encrypted
unsigned char header[10 + Aes_ctr_decryptor::NONCE_LEN];
std::cin.read(reinterpret_cast<char*>(header), sizeof(header));
if (!std::cin || std::cin.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) {
std::clog << "git-crypt: error: file not encrypted" << std::endl;
return 1;
}
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::process_stream(std::cin, std::cout, key->aes_key, nonce);
return 0;
}
int diff (int argc, char** argv)
{
const char* filename = 0;
const char* legacy_key_path = 0;
if (argc == 1) {
filename = argv[0];
} else if (argc == 2) {
legacy_key_path = argv[0];
filename = argv[1];
} else {
std::clog << "Usage: git-crypt diff FILENAME" << std::endl;
return 2;
}
Key_file key_file;
load_key(key_file, legacy_key_path);
// Open the file
std::ifstream in(filename, std::fstream::binary);
if (!in) {
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
unsigned char header[10 + Aes_ctr_decryptor::NONCE_LEN];
in.read(reinterpret_cast<char*>(header), sizeof(header));
if (!in || in.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) {
// File not encrypted - just copy it out to stdout
std::cout.write(reinterpret_cast<char*>(header), in.gcount()); // don't forget to include the header which we read!
std::cout << in.rdbuf();
return 0;
}
// Go ahead and decrypt it
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::process_stream(in, std::cout, key->aes_key, nonce);
return 0;
}
int init (int argc, char** argv)
{
if (argc == 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 != 0) {
std::clog << "Error: 'git-crypt init' takes no arguments." << std::endl;
return 2;
}
std::string internal_key_path(get_internal_key_path());
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)
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.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();
return 0;
}
int unlock (int argc, char** argv)
{
const char* symmetric_key_file = 0;
if (argc == 0) {
} else if (argc == 1) {
symmetric_key_file = argv[0];
} else {
std::clog << "Usage: git-crypt unlock [KEYFILE]" << std::endl;
return 2;
}
// 0. 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.
// Running 'git status' also serves as a check that the Git repo is accessible.
std::stringstream status_output;
get_git_status(status_output);
// 1. Check to see if HEAD exists. See below why we do this.
bool head_exists = check_if_head_exists();
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 << "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::string path_to_top(get_path_to_top());
// 3. Install the key
Key_file key_file;
if (symmetric_key_file) {
// Read from the symmetric key file
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/key/file'." << std::endl;
return 1;
}
} else {
// Decrypt GPG key from root of repo
std::string repo_keys_path(get_repo_keys_path());
std::vector<std::string> 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
if (!decrypt_repo_key(key_file, 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;
std::clog << "To see a list of GPG keys authorized to unlock this repository, run 'git-crypt ls-collabs'." << std::endl;
return 1;
}
}
std::string internal_key_path(get_internal_key_path());
// 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;
}
// 4. Configure git for git-crypt
configure_git_filters();
// 5. 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) {
// git checkout -f HEAD -- path/to/top
std::vector<std::string> command;
command.push_back("git");
command.push_back("checkout");
command.push_back("-f");
command.push_back("HEAD");
command.push_back("--");
if (path_to_top.empty()) {
command.push_back(".");
} else {
command.push_back(path_to_top);
}
if (!successful_exit(exec_command(command))) {
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;
}
int add_collab (int argc, char** argv)
{
if (argc == 0) {
std::clog << "Usage: git-crypt add-collab GPG_USER_ID [...]" << std::endl;
return 2;
}
// build a list of key fingerprints for every collaborator specified on the command line
std::vector<std::string> collab_keys;
for (int i = 0; i < argc; ++i) {
std::vector<std::string> 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);
const Key_file::Entry* key = key_file.get_latest();
if (!key) {
std::clog << "Error: key file is empty" << std::endl;
return 1;
}
std::string keys_path(get_repo_keys_path());
std::vector<std::string> new_files;
encrypt_repo_key(key_file.latest(), *key, collab_keys, keys_path, &new_files);
// add/commit the new files
if (!new_files.empty()) {
// git add NEW_FILE ...
std::vector<std::string> command;
command.push_back("git");
command.push_back("add");
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 ...
// TODO: add a command line option (-n perhaps) to inhibit committing
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<std::string>::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.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;
}
int rm_collab (int argc, char** argv) // TODO
{
std::clog << "Error: rm-collab is not yet implemented." << std::endl;
return 1;
}
int ls_collabs (int argc, char** argv) // TODO
{
// Sketch:
// Scan the sub-directories in .git-crypt/keys, outputting something like this:
// ====
// Key version 0:
// 0x143DE9B3F7316900 Andrew Ayer <andrew@example.com>
// 0x4E386D9C9C61702F ???
// Key version 1:
// 0x143DE9B3F7316900 Andrew Ayer <andrew@example.com>
// 0x1727274463D27F40 John Smith <smith@example.com>
// 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-collabs is not yet implemented." << std::endl;
return 1;
}
int export_key (int argc, char** argv)
{
// TODO: provide options to export only certain key versions
if (argc != 1) {
std::clog << "Usage: git-crypt export-key FILENAME" << std::endl;
return 2;
}
Key_file key_file;
load_key(key_file);
const char* out_file_name = argv[0];
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;
}
int keygen (int argc, char** argv)
{
if (argc != 1) {
std::clog << "Usage: git-crypt keygen KEYFILE" << std::endl;
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;
}
int migrate_key (int argc, char** argv)
{
if (argc != 1) {
std::clog << "Usage: git-crypt migrate-key KEYFILE" << std::endl;
return 2;
}
const char* key_file_name = argv[0];
Key_file key_file;
try {
if (std::strcmp(key_file_name, "-") == 0) {
key_file.load_legacy(std::cin);
key_file.store(std::cout);
} 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);
in.close();
std::string new_key_file_name(key_file_name);
new_key_file_name += ".new";
if (access(new_key_file_name.c_str(), F_OK) == 0) {
std::clog << new_key_file_name << ": File already exists" << std::endl;
return 1;
}
if (!key_file.store_to_file(new_key_file_name.c_str())) {
std::clog << "Error: " << new_key_file_name << ": unable to write key file" << std::endl;
return 1;
}
if (rename(new_key_file_name.c_str(), key_file_name) == -1) {
std::clog << "Error: " << key_file_name << ": " << strerror(errno) << std::endl;
unlink(new_key_file_name.c_str());
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;
}
int refresh (int argc, char** argv) // TODO: do a force checkout, much like in unlock
{
std::clog << "Error: refresh is not yet implemented." << std::endl;
return 1;
}