1 /*
2 * Copyright 2012, 2014 Andrew Ayer
3 *
4 * This file is part of git-crypt.
5 *
6 * git-crypt is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * git-crypt is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with git-crypt. If not, see <http://www.gnu.org/licenses/>.
18 *
19 * Additional permission under GNU GPL version 3 section 7:
20 *
21 * If you modify the Program, or any covered work, by linking or
22 * combining it with the OpenSSL project's OpenSSL library (or a
23 * modified version of that library), containing parts covered by the
24 * terms of the OpenSSL or SSLeay licenses, the licensors of the Program
25 * grant you additional permission to convey the resulting work.
26 * Corresponding Source for a non-source form of such a combination
27 * shall include the source code for the parts of OpenSSL used as well
28 * as that of the covered work.
29 */
30
31 #include "commands.hpp"
32 #include "crypto.hpp"
33 #include "util.hpp"
34 #include "key.hpp"
35 #include "gpg.hpp"
36 #include "parse_options.hpp"
37 #include "coprocess.hpp"
38 #include <unistd.h>
39 #include <stdint.h>
40 #include <algorithm>
41 #include <string>
42 #include <fstream>
43 #include <sstream>
44 #include <iostream>
45 #include <cstddef>
46 #include <cstring>
47 #include <cctype>
48 #include <stdio.h>
49 #include <string.h>
50 #include <errno.h>
51 #include <exception>
52 #include <vector>
53
attribute_name(const char * key_name)54 static std::string attribute_name (const char* key_name)
55 {
56 if (key_name) {
57 // named key
58 return std::string("git-crypt-") + key_name;
59 } else {
60 // default key
61 return "git-crypt";
62 }
63 }
64
git_version_string()65 static std::string git_version_string ()
66 {
67 std::vector<std::string> command;
68 command.push_back("/usr/local/bin/git");
69 command.push_back("version");
70
71 std::stringstream output;
72 if (!successful_exit(exec_command(command, output))) {
73 throw Error("'git version' failed - is Git installed?");
74 }
75 std::string word;
76 output >> word; // "/usr/local/bin/git"
77 output >> word; // "version"
78 output >> word; // "1.7.10.4"
79 return word;
80 }
81
parse_version(const std::string & str)82 static std::vector<int> parse_version (const std::string& str)
83 {
84 std::istringstream in(str);
85 std::vector<int> version;
86 std::string component;
87 while (std::getline(in, component, '.')) {
88 version.push_back(std::atoi(component.c_str()));
89 }
90 return version;
91 }
92
git_version()93 static const std::vector<int>& git_version ()
94 {
95 static const std::vector<int> version(parse_version(git_version_string()));
96 return version;
97 }
98
make_version(int a,int b,int c)99 static std::vector<int> make_version (int a, int b, int c)
100 {
101 std::vector<int> version;
102 version.push_back(a);
103 version.push_back(b);
104 version.push_back(c);
105 return version;
106 }
107
git_config(const std::string & name,const std::string & value)108 static void git_config (const std::string& name, const std::string& value)
109 {
110 std::vector<std::string> command;
111 command.push_back("/usr/local/bin/git");
112 command.push_back("config");
113 command.push_back(name);
114 command.push_back(value);
115
116 if (!successful_exit(exec_command(command))) {
117 throw Error("'git config' failed");
118 }
119 }
120
git_has_config(const std::string & name)121 static bool git_has_config (const std::string& name)
122 {
123 std::vector<std::string> command;
124 command.push_back("/usr/local/bin/git");
125 command.push_back("config");
126 command.push_back("--get-all");
127 command.push_back(name);
128
129 std::stringstream output;
130 switch (exit_status(exec_command(command, output))) {
131 case 0: return true;
132 case 1: return false;
133 default: throw Error("'git config' failed");
134 }
135 }
136
git_deconfig(const std::string & name)137 static void git_deconfig (const std::string& name)
138 {
139 std::vector<std::string> command;
140 command.push_back("/usr/local/bin/git");
141 command.push_back("config");
142 command.push_back("--remove-section");
143 command.push_back(name);
144
145 if (!successful_exit(exec_command(command))) {
146 throw Error("'git config' failed");
147 }
148 }
149
configure_git_filters(const char * key_name)150 static void configure_git_filters (const char* key_name)
151 {
152 std::string escaped_git_crypt_path(escape_shell_arg(our_exe_path()));
153
154 if (key_name) {
155 // Note: key_name contains only shell-safe characters so it need not be escaped.
156 git_config(std::string("filter.git-crypt-") + key_name + ".smudge",
157 escaped_git_crypt_path + " smudge --key-name=" + key_name);
158 git_config(std::string("filter.git-crypt-") + key_name + ".clean",
159 escaped_git_crypt_path + " clean --key-name=" + key_name);
160 git_config(std::string("filter.git-crypt-") + key_name + ".required", "true");
161 git_config(std::string("diff.git-crypt-") + key_name + ".textconv",
162 escaped_git_crypt_path + " diff --key-name=" + key_name);
163 } else {
164 git_config("filter.git-crypt.smudge", escaped_git_crypt_path + " smudge");
165 git_config("filter.git-crypt.clean", escaped_git_crypt_path + " clean");
166 git_config("filter.git-crypt.required", "true");
167 git_config("diff.git-crypt.textconv", escaped_git_crypt_path + " diff");
168 }
169 }
170
deconfigure_git_filters(const char * key_name)171 static void deconfigure_git_filters (const char* key_name)
172 {
173 // deconfigure the git-crypt filters
174 if (git_has_config("filter." + attribute_name(key_name) + ".smudge") ||
175 git_has_config("filter." + attribute_name(key_name) + ".clean") ||
176 git_has_config("filter." + attribute_name(key_name) + ".required")) {
177
178 git_deconfig("filter." + attribute_name(key_name));
179 }
180
181 if (git_has_config("diff." + attribute_name(key_name) + ".textconv")) {
182 git_deconfig("diff." + attribute_name(key_name));
183 }
184 }
185
git_checkout(const std::vector<std::string> & paths)186 static bool git_checkout (const std::vector<std::string>& paths)
187 {
188 std::vector<std::string> command;
189
190 command.push_back("/usr/local/bin/git");
191 command.push_back("checkout");
192 command.push_back("--");
193
194 for (std::vector<std::string>::const_iterator path(paths.begin()); path != paths.end(); ++path) {
195 command.push_back(*path);
196 }
197
198 if (!successful_exit(exec_command(command))) {
199 return false;
200 }
201
202 return true;
203 }
204
same_key_name(const char * a,const char * b)205 static bool same_key_name (const char* a, const char* b)
206 {
207 return (!a && !b) || (a && b && std::strcmp(a, b) == 0);
208 }
209
validate_key_name_or_throw(const char * key_name)210 static void validate_key_name_or_throw (const char* key_name)
211 {
212 std::string reason;
213 if (!validate_key_name(key_name, &reason)) {
214 throw Error(reason);
215 }
216 }
217
get_internal_state_path()218 static std::string get_internal_state_path ()
219 {
220 // git rev-parse --git-dir
221 std::vector<std::string> command;
222 command.push_back("/usr/local/bin/git");
223 command.push_back("rev-parse");
224 command.push_back("--git-dir");
225
226 std::stringstream output;
227
228 if (!successful_exit(exec_command(command, output))) {
229 throw Error("'git rev-parse --git-dir' failed - is this a Git repository?");
230 }
231
232 std::string path;
233 std::getline(output, path);
234 path += "/git-crypt";
235
236 return path;
237 }
238
get_internal_keys_path(const std::string & internal_state_path)239 static std::string get_internal_keys_path (const std::string& internal_state_path)
240 {
241 return internal_state_path + "/keys";
242 }
243
get_internal_keys_path()244 static std::string get_internal_keys_path ()
245 {
246 return get_internal_keys_path(get_internal_state_path());
247 }
248
get_internal_key_path(const char * key_name)249 static std::string get_internal_key_path (const char* key_name)
250 {
251 std::string path(get_internal_keys_path());
252 path += "/";
253 path += key_name ? key_name : "default";
254
255 return path;
256 }
257
get_git_config(const std::string & name)258 std::string get_git_config (const std::string& name)
259 {
260 // git config --get
261 std::vector<std::string> command;
262 command.push_back("/usr/local/bin/git");
263 command.push_back("config");
264 command.push_back("--get");
265 command.push_back(name);
266
267 std::stringstream output;
268
269 if (!successful_exit(exec_command(command, output))) {
270 throw Error("'git config' missing value for key '" + name +"'");
271 }
272
273 std::string value;
274 std::getline(output, value);
275
276 return value;
277 }
278
get_repo_state_path()279 static std::string get_repo_state_path ()
280 {
281 // git rev-parse --show-toplevel
282 std::vector<std::string> command;
283 command.push_back("/usr/local/bin/git");
284 command.push_back("rev-parse");
285 command.push_back("--show-toplevel");
286
287 std::stringstream output;
288
289 if (!successful_exit(exec_command(command, output))) {
290 throw Error("'git rev-parse --show-toplevel' failed - is this a Git repository?");
291 }
292
293 std::string path;
294 std::getline(output, path);
295
296 if (path.empty()) {
297 // could happen for a bare repo
298 throw Error("Could not determine Git working tree - is this a non-bare repo?");
299 }
300
301 // Check if the repo state dir has been explicitly configured. If so, use that in path construction.
302 if (git_has_config("git-crypt.repoStateDir")) {
303 std::string repoStateDir = get_git_config("git-crypt.repoStateDir");
304
305 // The repoStateDir value must always be relative to git work tree to ensure the repoStateDir can be committed
306 // along with the remainder of the repository.
307 path += '/' + repoStateDir;
308 } else {
309 // There is no explicitly configured repo state dir configured, so use the default.
310 path += "/.git-crypt";
311 }
312
313 return path;
314 }
315
get_repo_keys_path(const std::string & repo_state_path)316 static std::string get_repo_keys_path (const std::string& repo_state_path)
317 {
318 return repo_state_path + "/keys";
319 }
320
get_repo_keys_path()321 static std::string get_repo_keys_path ()
322 {
323 return get_repo_keys_path(get_repo_state_path());
324 }
325
get_path_to_top()326 static std::string get_path_to_top ()
327 {
328 // git rev-parse --show-cdup
329 std::vector<std::string> command;
330 command.push_back("/usr/local/bin/git");
331 command.push_back("rev-parse");
332 command.push_back("--show-cdup");
333
334 std::stringstream output;
335
336 if (!successful_exit(exec_command(command, output))) {
337 throw Error("'git rev-parse --show-cdup' failed - is this a Git repository?");
338 }
339
340 std::string path_to_top;
341 std::getline(output, path_to_top);
342
343 return path_to_top;
344 }
345
get_git_status(std::ostream & output)346 static void get_git_status (std::ostream& output)
347 {
348 // git status -uno --porcelain
349 std::vector<std::string> command;
350 command.push_back("/usr/local/bin/git");
351 command.push_back("status");
352 command.push_back("-uno"); // don't show untracked files
353 command.push_back("--porcelain");
354
355 if (!successful_exit(exec_command(command, output))) {
356 throw Error("'git status' failed - is this a Git repository?");
357 }
358 }
359
360 // returns filter and diff attributes as a pair
get_file_attributes(const std::string & filename)361 static std::pair<std::string, std::string> get_file_attributes (const std::string& filename)
362 {
363 // git check-attr filter diff -- filename
364 std::vector<std::string> command;
365 command.push_back("/usr/local/bin/git");
366 command.push_back("check-attr");
367 command.push_back("filter");
368 command.push_back("diff");
369 command.push_back("--");
370 command.push_back(filename);
371
372 std::stringstream output;
373 if (!successful_exit(exec_command(command, output))) {
374 throw Error("'git check-attr' failed - is this a Git repository?");
375 }
376
377 std::string filter_attr;
378 std::string diff_attr;
379
380 std::string line;
381 // Example output:
382 // filename: filter: git-crypt
383 // filename: diff: git-crypt
384 while (std::getline(output, line)) {
385 // filename might contain ": ", so parse line backwards
386 // filename: attr_name: attr_value
387 // ^name_pos ^value_pos
388 const std::string::size_type value_pos(line.rfind(": "));
389 if (value_pos == std::string::npos || value_pos == 0) {
390 continue;
391 }
392 const std::string::size_type name_pos(line.rfind(": ", value_pos - 1));
393 if (name_pos == std::string::npos) {
394 continue;
395 }
396
397 const std::string attr_name(line.substr(name_pos + 2, value_pos - (name_pos + 2)));
398 const std::string attr_value(line.substr(value_pos + 2));
399
400 if (attr_value != "unspecified" && attr_value != "unset" && attr_value != "set") {
401 if (attr_name == "filter") {
402 filter_attr = attr_value;
403 } else if (attr_name == "diff") {
404 diff_attr = attr_value;
405 }
406 }
407 }
408
409 return std::make_pair(filter_attr, diff_attr);
410 }
411
412 // returns filter and diff attributes as a pair
get_file_attributes(const std::string & filename,std::ostream & check_attr_stdin,std::istream & check_attr_stdout)413 static std::pair<std::string, std::string> get_file_attributes (const std::string& filename, std::ostream& check_attr_stdin, std::istream& check_attr_stdout)
414 {
415 check_attr_stdin << filename << '\0' << std::flush;
416
417 std::string filter_attr;
418 std::string diff_attr;
419
420 // Example output:
421 // filename\0filter\0git-crypt\0filename\0diff\0git-crypt\0
422 for (int i = 0; i < 2; ++i) {
423 std::string filename;
424 std::string attr_name;
425 std::string attr_value;
426 std::getline(check_attr_stdout, filename, '\0');
427 std::getline(check_attr_stdout, attr_name, '\0');
428 std::getline(check_attr_stdout, attr_value, '\0');
429
430 if (attr_value != "unspecified" && attr_value != "unset" && attr_value != "set") {
431 if (attr_name == "filter") {
432 filter_attr = attr_value;
433 } else if (attr_name == "diff") {
434 diff_attr = attr_value;
435 }
436 }
437 }
438
439 return std::make_pair(filter_attr, diff_attr);
440 }
441
check_if_blob_is_encrypted(const std::string & object_id)442 static bool check_if_blob_is_encrypted (const std::string& object_id)
443 {
444 // git cat-file blob object_id
445
446 std::vector<std::string> command;
447 command.push_back("/usr/local/bin/git");
448 command.push_back("cat-file");
449 command.push_back("blob");
450 command.push_back(object_id);
451
452 // TODO: do this more efficiently - don't read entire command output into buffer, only read what we need
453 std::stringstream output;
454 if (!successful_exit(exec_command(command, output))) {
455 throw Error("'git cat-file' failed - is this a Git repository?");
456 }
457
458 char header[10];
459 output.read(header, sizeof(header));
460 return output.gcount() == sizeof(header) && std::memcmp(header, "\0GITCRYPT\0", 10) == 0;
461 }
462
check_if_file_is_encrypted(const std::string & filename)463 static bool check_if_file_is_encrypted (const std::string& filename)
464 {
465 // git ls-files -sz filename
466 std::vector<std::string> command;
467 command.push_back("/usr/local/bin/git");
468 command.push_back("ls-files");
469 command.push_back("-sz");
470 command.push_back("--");
471 command.push_back(filename);
472
473 std::stringstream output;
474 if (!successful_exit(exec_command(command, output))) {
475 throw Error("'git ls-files' failed - is this a Git repository?");
476 }
477
478 if (output.peek() == -1) {
479 return false;
480 }
481
482 std::string mode;
483 std::string object_id;
484 output >> mode >> object_id;
485
486 return check_if_blob_is_encrypted(object_id);
487 }
488
is_git_file_mode(const std::string & mode)489 static bool is_git_file_mode (const std::string& mode)
490 {
491 return (std::strtoul(mode.c_str(), nullptr, 8) & 0170000) == 0100000;
492 }
493
get_encrypted_files(std::vector<std::string> & files,const char * key_name)494 static void get_encrypted_files (std::vector<std::string>& files, const char* key_name)
495 {
496 // git ls-files -cz -- path_to_top
497 std::vector<std::string> ls_files_command;
498 ls_files_command.push_back("/usr/local/bin/git");
499 ls_files_command.push_back("ls-files");
500 ls_files_command.push_back("-csz");
501 ls_files_command.push_back("--");
502 const std::string path_to_top(get_path_to_top());
503 if (!path_to_top.empty()) {
504 ls_files_command.push_back(path_to_top);
505 }
506
507 Coprocess ls_files;
508 std::istream* ls_files_stdout = ls_files.stdout_pipe();
509 ls_files.spawn(ls_files_command);
510
511 Coprocess check_attr;
512 std::ostream* check_attr_stdin = nullptr;
513 std::istream* check_attr_stdout = nullptr;
514 if (git_version() >= make_version(1, 8, 5)) {
515 // In Git 1.8.5 (released 27 Nov 2013) and higher, we use a single `git check-attr` process
516 // to get the attributes of all files at once. In prior versions, we have to fork and exec
517 // a separate `git check-attr` process for each file, since -z and --stdin aren't supported.
518 // In a repository with thousands of files, this results in an almost 100x speedup.
519 std::vector<std::string> check_attr_command;
520 check_attr_command.push_back("/usr/local/bin/git");
521 check_attr_command.push_back("check-attr");
522 check_attr_command.push_back("--stdin");
523 check_attr_command.push_back("-z");
524 check_attr_command.push_back("filter");
525 check_attr_command.push_back("diff");
526
527 check_attr_stdin = check_attr.stdin_pipe();
528 check_attr_stdout = check_attr.stdout_pipe();
529 check_attr.spawn(check_attr_command);
530 }
531
532 while (ls_files_stdout->peek() != -1) {
533 std::string mode;
534 std::string object_id;
535 std::string stage;
536 std::string filename;
537 *ls_files_stdout >> mode >> object_id >> stage >> std::ws;
538 std::getline(*ls_files_stdout, filename, '\0');
539
540 if (is_git_file_mode(mode)) {
541 std::string filter_attribute;
542
543 if (check_attr_stdin) {
544 filter_attribute = get_file_attributes(filename, *check_attr_stdin, *check_attr_stdout).first;
545 } else {
546 filter_attribute = get_file_attributes(filename).first;
547 }
548
549 if (filter_attribute == attribute_name(key_name)) {
550 files.push_back(filename);
551 }
552 }
553 }
554
555 if (!successful_exit(ls_files.wait())) {
556 throw Error("'git ls-files' failed - is this a Git repository?");
557 }
558
559 if (check_attr_stdin) {
560 check_attr.close_stdin();
561 if (!successful_exit(check_attr.wait())) {
562 throw Error("'git check-attr' failed - is this a Git repository?");
563 }
564 }
565 }
566
load_key(Key_file & key_file,const char * key_name,const char * key_path=0,const char * legacy_path=0)567 static void load_key (Key_file& key_file, const char* key_name, const char* key_path =0, const char* legacy_path =0)
568 {
569 if (legacy_path) {
570 std::ifstream key_file_in(legacy_path, std::fstream::binary);
571 if (!key_file_in) {
572 throw Error(std::string("Unable to open key file: ") + legacy_path);
573 }
574 key_file.load_legacy(key_file_in);
575 } else if (key_path) {
576 std::ifstream key_file_in(key_path, std::fstream::binary);
577 if (!key_file_in) {
578 throw Error(std::string("Unable to open key file: ") + key_path);
579 }
580 key_file.load(key_file_in);
581 } else {
582 std::ifstream key_file_in(get_internal_key_path(key_name).c_str(), std::fstream::binary);
583 if (!key_file_in) {
584 // TODO: include key name in error message
585 throw Error("Unable to open key file - have you unlocked/initialized this repository yet?");
586 }
587 key_file.load(key_file_in);
588 }
589 }
590
decrypt_repo_key(Key_file & key_file,const char * key_name,uint32_t key_version,const std::vector<std::string> & secret_keys,const std::string & keys_path)591 static bool decrypt_repo_key (Key_file& key_file, const char* key_name, uint32_t key_version, const std::vector<std::string>& secret_keys, const std::string& keys_path)
592 {
593 std::exception_ptr gpg_error;
594
595 for (std::vector<std::string>::const_iterator seckey(secret_keys.begin()); seckey != secret_keys.end(); ++seckey) {
596 std::ostringstream path_builder;
597 path_builder << keys_path << '/' << (key_name ? key_name : "default") << '/' << key_version << '/' << *seckey << ".gpg";
598 std::string path(path_builder.str());
599 if (access(path.c_str(), F_OK) == 0) {
600 std::stringstream decrypted_contents;
601 try {
602 gpg_decrypt_from_file(path, decrypted_contents);
603 } catch (const Gpg_error&) {
604 gpg_error = std::current_exception();
605 continue;
606 }
607 Key_file this_version_key_file;
608 this_version_key_file.load(decrypted_contents);
609 const Key_file::Entry* this_version_entry = this_version_key_file.get(key_version);
610 if (!this_version_entry) {
611 throw Error("GPG-encrypted keyfile is malformed because it does not contain expected key version");
612 }
613 if (!same_key_name(key_name, this_version_key_file.get_key_name())) {
614 throw Error("GPG-encrypted keyfile is malformed because it does not contain expected key name");
615 }
616 key_file.set_key_name(key_name);
617 key_file.add(*this_version_entry);
618 return true;
619 }
620 }
621
622 if (gpg_error) {
623 std::rethrow_exception(gpg_error);
624 }
625
626 return false;
627 }
628
decrypt_repo_keys(std::vector<Key_file> & key_files,uint32_t key_version,const std::vector<std::string> & secret_keys,const std::string & keys_path)629 static bool decrypt_repo_keys (std::vector<Key_file>& key_files, uint32_t key_version, const std::vector<std::string>& secret_keys, const std::string& keys_path)
630 {
631 bool successful = false;
632 std::vector<std::string> dirents;
633
634 if (access(keys_path.c_str(), F_OK) == 0) {
635 dirents = get_directory_contents(keys_path.c_str());
636 }
637
638 for (std::vector<std::string>::const_iterator dirent(dirents.begin()); dirent != dirents.end(); ++dirent) {
639 const char* key_name = 0;
640 if (*dirent != "default") {
641 if (!validate_key_name(dirent->c_str())) {
642 continue;
643 }
644 key_name = dirent->c_str();
645 }
646
647 Key_file key_file;
648 if (decrypt_repo_key(key_file, key_name, key_version, secret_keys, keys_path)) {
649 key_files.push_back(key_file);
650 successful = true;
651 }
652 }
653 return successful;
654 }
655
encrypt_repo_key(const char * key_name,const Key_file::Entry & key,const std::vector<std::pair<std::string,bool>> & collab_keys,const std::string & keys_path,std::vector<std::string> * new_files)656 static void encrypt_repo_key (const char* key_name, const Key_file::Entry& key, const std::vector<std::pair<std::string, bool> >& collab_keys, const std::string& keys_path, std::vector<std::string>* new_files)
657 {
658 std::string key_file_data;
659 {
660 Key_file this_version_key_file;
661 this_version_key_file.set_key_name(key_name);
662 this_version_key_file.add(key);
663 key_file_data = this_version_key_file.store_to_string();
664 }
665
666 for (std::vector<std::pair<std::string, bool> >::const_iterator collab(collab_keys.begin()); collab != collab_keys.end(); ++collab) {
667 const std::string& fingerprint(collab->first);
668 const bool key_is_trusted(collab->second);
669 std::ostringstream path_builder;
670 path_builder << keys_path << '/' << (key_name ? key_name : "default") << '/' << key.version << '/' << fingerprint << ".gpg";
671 std::string path(path_builder.str());
672
673 if (access(path.c_str(), F_OK) == 0) {
674 continue;
675 }
676
677 mkdir_parent(path);
678 gpg_encrypt_to_file(path, fingerprint, key_is_trusted, key_file_data.data(), key_file_data.size());
679 new_files->push_back(path);
680 }
681 }
682
parse_plumbing_options(const char ** key_name,const char ** key_file,int argc,const char ** argv)683 static int parse_plumbing_options (const char** key_name, const char** key_file, int argc, const char** argv)
684 {
685 Options_list options;
686 options.push_back(Option_def("-k", key_name));
687 options.push_back(Option_def("--key-name", key_name));
688 options.push_back(Option_def("--key-file", key_file));
689
690 return parse_options(options, argc, argv);
691 }
692
693 // Encrypt contents of stdin and write to stdout
clean(int argc,const char ** argv)694 int clean (int argc, const char** argv)
695 {
696 const char* key_name = 0;
697 const char* key_path = 0;
698 const char* legacy_key_path = 0;
699
700 int argi = parse_plumbing_options(&key_name, &key_path, argc, argv);
701 if (argc - argi == 0) {
702 } else if (!key_name && !key_path && argc - argi == 1) { // Deprecated - for compatibility with pre-0.4
703 legacy_key_path = argv[argi];
704 } else {
705 std::clog << "Usage: git-crypt clean [--key-name=NAME] [--key-file=PATH]" << std::endl;
706 return 2;
707 }
708 Key_file key_file;
709 load_key(key_file, key_name, key_path, legacy_key_path);
710
711 const Key_file::Entry* key = key_file.get_latest();
712 if (!key) {
713 std::clog << "git-crypt: error: key file is empty" << std::endl;
714 return 1;
715 }
716
717 // Read the entire file
718
719 Hmac_sha1_state hmac(key->hmac_key, HMAC_KEY_LEN); // Calculate the file's SHA1 HMAC as we go
720 uint64_t file_size = 0; // Keep track of the length, make sure it doesn't get too big
721 std::string file_contents; // First 8MB or so of the file go here
722 temp_fstream temp_file; // The rest of the file spills into a temporary file on disk
723 temp_file.exceptions(std::fstream::badbit);
724
725 char buffer[1024];
726
727 while (std::cin && file_size < Aes_ctr_encryptor::MAX_CRYPT_BYTES) {
728 std::cin.read(buffer, sizeof(buffer));
729
730 const size_t bytes_read = std::cin.gcount();
731
732 hmac.add(reinterpret_cast<unsigned char*>(buffer), bytes_read);
733 file_size += bytes_read;
734
735 if (file_size <= 8388608) {
736 file_contents.append(buffer, bytes_read);
737 } else {
738 if (!temp_file.is_open()) {
739 temp_file.open(std::fstream::in | std::fstream::out | std::fstream::binary | std::fstream::app);
740 }
741 temp_file.write(buffer, bytes_read);
742 }
743 }
744
745 // Make sure the file isn't so large we'll overflow the counter value (which would doom security)
746 if (file_size >= Aes_ctr_encryptor::MAX_CRYPT_BYTES) {
747 std::clog << "git-crypt: error: file too long to encrypt securely" << std::endl;
748 return 1;
749 }
750
751 // We use an HMAC of the file as the encryption nonce (IV) for CTR mode.
752 // By using a hash of the file we ensure that the encryption is
753 // deterministic so git doesn't think the file has changed when it really
754 // hasn't. CTR mode with a synthetic IV is provably semantically secure
755 // under deterministic CPA as long as the synthetic IV is derived from a
756 // secure PRF applied to the message. Since HMAC-SHA1 is a secure PRF, this
757 // encryption scheme is semantically secure under deterministic CPA.
758 //
759 // Informally, consider that if a file changes just a tiny bit, the IV will
760 // be completely different, resulting in a completely different ciphertext
761 // that leaks no information about the similarities of the plaintexts. Also,
762 // since we're using the output from a secure hash function plus a counter
763 // as the input to our block cipher, we should never have a situation where
764 // two different plaintext blocks get encrypted with the same CTR value. A
765 // nonce will be reused only if the entire file is the same, which leaks no
766 // information except that the files are the same.
767 //
768 // To prevent an attacker from building a dictionary of hash values and then
769 // looking up the nonce (which must be stored in the clear to allow for
770 // decryption), we use an HMAC as opposed to a straight hash.
771
772 // Note: Hmac_sha1_state::LEN >= Aes_ctr_encryptor::NONCE_LEN
773
774 unsigned char digest[Hmac_sha1_state::LEN];
775 hmac.get(digest);
776
777 // Write a header that...
778 std::cout.write("\0GITCRYPT\0", 10); // ...identifies this as an encrypted file
779 std::cout.write(reinterpret_cast<char*>(digest), Aes_ctr_encryptor::NONCE_LEN); // ...includes the nonce
780
781 // Now encrypt the file and write to stdout
782 Aes_ctr_encryptor aes(key->aes_key, digest);
783
784 // First read from the in-memory copy
785 const unsigned char* file_data = reinterpret_cast<const unsigned char*>(file_contents.data());
786 size_t file_data_len = file_contents.size();
787 while (file_data_len > 0) {
788 const size_t buffer_len = std::min(sizeof(buffer), file_data_len);
789 aes.process(file_data, reinterpret_cast<unsigned char*>(buffer), buffer_len);
790 std::cout.write(buffer, buffer_len);
791 file_data += buffer_len;
792 file_data_len -= buffer_len;
793 }
794
795 // Then read from the temporary file if applicable
796 if (temp_file.is_open()) {
797 temp_file.seekg(0);
798 while (temp_file.peek() != -1) {
799 temp_file.read(buffer, sizeof(buffer));
800
801 const size_t buffer_len = temp_file.gcount();
802
803 aes.process(reinterpret_cast<unsigned char*>(buffer),
804 reinterpret_cast<unsigned char*>(buffer),
805 buffer_len);
806 std::cout.write(buffer, buffer_len);
807 }
808 }
809
810 return 0;
811 }
812
decrypt_file_to_stdout(const Key_file & key_file,const unsigned char * header,std::istream & in)813 static int decrypt_file_to_stdout (const Key_file& key_file, const unsigned char* header, std::istream& in)
814 {
815 const unsigned char* nonce = header + 10;
816 uint32_t key_version = 0; // TODO: get the version from the file header
817
818 const Key_file::Entry* key = key_file.get(key_version);
819 if (!key) {
820 std::clog << "git-crypt: error: key version " << key_version << " not available - please unlock with the latest version of the key." << std::endl;
821 return 1;
822 }
823
824 Aes_ctr_decryptor aes(key->aes_key, nonce);
825 Hmac_sha1_state hmac(key->hmac_key, HMAC_KEY_LEN);
826 while (in) {
827 unsigned char buffer[1024];
828 in.read(reinterpret_cast<char*>(buffer), sizeof(buffer));
829 aes.process(buffer, buffer, in.gcount());
830 hmac.add(buffer, in.gcount());
831 std::cout.write(reinterpret_cast<char*>(buffer), in.gcount());
832 }
833
834 unsigned char digest[Hmac_sha1_state::LEN];
835 hmac.get(digest);
836 if (!leakless_equals(digest, nonce, Aes_ctr_decryptor::NONCE_LEN)) {
837 std::clog << "git-crypt: error: encrypted file has been tampered with!" << std::endl;
838 // Although we've already written the tampered file to stdout, exiting
839 // with a non-zero status will tell git the file has not been filtered,
840 // so git will not replace it.
841 return 1;
842 }
843
844 return 0;
845 }
846
847 // Decrypt contents of stdin and write to stdout
smudge(int argc,const char ** argv)848 int smudge (int argc, const char** argv)
849 {
850 const char* key_name = 0;
851 const char* key_path = 0;
852 const char* legacy_key_path = 0;
853
854 int argi = parse_plumbing_options(&key_name, &key_path, argc, argv);
855 if (argc - argi == 0) {
856 } else if (!key_name && !key_path && argc - argi == 1) { // Deprecated - for compatibility with pre-0.4
857 legacy_key_path = argv[argi];
858 } else {
859 std::clog << "Usage: git-crypt smudge [--key-name=NAME] [--key-file=PATH]" << std::endl;
860 return 2;
861 }
862 Key_file key_file;
863 load_key(key_file, key_name, key_path, legacy_key_path);
864
865 // Read the header to get the nonce and make sure it's actually encrypted
866 unsigned char header[10 + Aes_ctr_decryptor::NONCE_LEN];
867 std::cin.read(reinterpret_cast<char*>(header), sizeof(header));
868 if (std::cin.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) {
869 // File not encrypted - just copy it out to stdout
870 std::clog << "git-crypt: Warning: file not encrypted" << std::endl;
871 std::clog << "git-crypt: Run 'git-crypt status' to make sure all files are properly encrypted." << std::endl;
872 std::clog << "git-crypt: If 'git-crypt status' reports no problems, then an older version of" << std::endl;
873 std::clog << "git-crypt: this file may be unencrypted in the repository's history. If this" << std::endl;
874 std::clog << "git-crypt: file contains sensitive information, you can use 'git filter-branch'" << std::endl;
875 std::clog << "git-crypt: to remove its old versions from the history." << std::endl;
876 std::cout.write(reinterpret_cast<char*>(header), std::cin.gcount()); // include the bytes which we already read
877 std::cout << std::cin.rdbuf();
878 return 0;
879 }
880
881 return decrypt_file_to_stdout(key_file, header, std::cin);
882 }
883
diff(int argc,const char ** argv)884 int diff (int argc, const char** argv)
885 {
886 const char* key_name = 0;
887 const char* key_path = 0;
888 const char* filename = 0;
889 const char* legacy_key_path = 0;
890
891 int argi = parse_plumbing_options(&key_name, &key_path, argc, argv);
892 if (argc - argi == 1) {
893 filename = argv[argi];
894 } else if (!key_name && !key_path && argc - argi == 2) { // Deprecated - for compatibility with pre-0.4
895 legacy_key_path = argv[argi];
896 filename = argv[argi + 1];
897 } else {
898 std::clog << "Usage: git-crypt diff [--key-name=NAME] [--key-file=PATH] FILENAME" << std::endl;
899 return 2;
900 }
901 Key_file key_file;
902 load_key(key_file, key_name, key_path, legacy_key_path);
903
904 // Open the file
905 std::ifstream in(filename, std::fstream::binary);
906 if (!in) {
907 std::clog << "git-crypt: " << filename << ": unable to open for reading" << std::endl;
908 return 1;
909 }
910 in.exceptions(std::fstream::badbit);
911
912 // Read the header to get the nonce and determine if it's actually encrypted
913 unsigned char header[10 + Aes_ctr_decryptor::NONCE_LEN];
914 in.read(reinterpret_cast<char*>(header), sizeof(header));
915 if (in.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) {
916 // File not encrypted - just copy it out to stdout
917 std::cout.write(reinterpret_cast<char*>(header), in.gcount()); // include the bytes which we already read
918 std::cout << in.rdbuf();
919 return 0;
920 }
921
922 // Go ahead and decrypt it
923 return decrypt_file_to_stdout(key_file, header, in);
924 }
925
help_init(std::ostream & out)926 void help_init (std::ostream& out)
927 {
928 // |--------------------------------------------------------------------------------| 80 chars
929 out << "Usage: git-crypt init [OPTIONS]" << std::endl;
930 out << std::endl;
931 out << " -k, --key-name KEYNAME Initialize the given key, instead of the default" << std::endl;
932 out << std::endl;
933 }
934
init(int argc,const char ** argv)935 int init (int argc, const char** argv)
936 {
937 const char* key_name = 0;
938 Options_list options;
939 options.push_back(Option_def("-k", &key_name));
940 options.push_back(Option_def("--key-name", &key_name));
941
942 int argi = parse_options(options, argc, argv);
943
944 if (!key_name && argc - argi == 1) {
945 std::clog << "Warning: 'git-crypt init' with a key file is deprecated as of git-crypt 0.4" << std::endl;
946 std::clog << "and will be removed in a future release. Please get in the habit of using" << std::endl;
947 std::clog << "'git-crypt unlock KEYFILE' instead." << std::endl;
948 return unlock(argc, argv);
949 }
950 if (argc - argi != 0) {
951 std::clog << "Error: git-crypt init takes no arguments" << std::endl;
952 help_init(std::clog);
953 return 2;
954 }
955
956 if (key_name) {
957 validate_key_name_or_throw(key_name);
958 }
959
960 std::string internal_key_path(get_internal_key_path(key_name));
961 if (access(internal_key_path.c_str(), F_OK) == 0) {
962 // TODO: add a -f option to reinitialize the repo anyways (this should probably imply a refresh)
963 // TODO: include key_name in error message
964 std::clog << "Error: this repository has already been initialized with git-crypt." << std::endl;
965 return 1;
966 }
967
968 // 1. Generate a key and install it
969 std::clog << "Generating key..." << std::endl;
970 Key_file key_file;
971 key_file.set_key_name(key_name);
972 key_file.generate();
973
974 mkdir_parent(internal_key_path);
975 if (!key_file.store_to_file(internal_key_path.c_str())) {
976 std::clog << "Error: " << internal_key_path << ": unable to write key file" << std::endl;
977 return 1;
978 }
979
980 // 2. Configure git for git-crypt
981 configure_git_filters(key_name);
982
983 return 0;
984 }
985
help_unlock(std::ostream & out)986 void help_unlock (std::ostream& out)
987 {
988 // |--------------------------------------------------------------------------------| 80 chars
989 out << "Usage: git-crypt unlock" << std::endl;
990 out << " or: git-crypt unlock KEY_FILE ..." << std::endl;
991 }
unlock(int argc,const char ** argv)992 int unlock (int argc, const char** argv)
993 {
994 // 1. Make sure working directory is clean (ignoring untracked files)
995 // We do this because we check out files later, and we don't want the
996 // user to lose any changes. (TODO: only care if encrypted files are
997 // modified, since we only check out encrypted files)
998
999 // Running 'git status' also serves as a check that the Git repo is accessible.
1000
1001 std::stringstream status_output;
1002 get_git_status(status_output);
1003 if (status_output.peek() != -1) {
1004 std::clog << "Error: Working directory not clean." << std::endl;
1005 std::clog << "Please commit your changes or 'git stash' them before running 'git-crypt unlock'." << std::endl;
1006 return 1;
1007 }
1008
1009 // 2. Load the key(s)
1010 std::vector<Key_file> key_files;
1011 if (argc > 0) {
1012 // Read from the symmetric key file(s)
1013
1014 for (int argi = 0; argi < argc; ++argi) {
1015 const char* symmetric_key_file = argv[argi];
1016 Key_file key_file;
1017
1018 try {
1019 if (std::strcmp(symmetric_key_file, "-") == 0) {
1020 key_file.load(std::cin);
1021 } else {
1022 if (!key_file.load_from_file(symmetric_key_file)) {
1023 std::clog << "Error: " << symmetric_key_file << ": unable to read key file" << std::endl;
1024 return 1;
1025 }
1026 }
1027 } catch (Key_file::Incompatible) {
1028 std::clog << "Error: " << symmetric_key_file << " is in an incompatible format" << std::endl;
1029 std::clog << "Please upgrade to a newer version of git-crypt." << std::endl;
1030 return 1;
1031 } catch (Key_file::Malformed) {
1032 std::clog << "Error: " << symmetric_key_file << ": not a valid git-crypt key file" << std::endl;
1033 std::clog << "If this key was created prior to git-crypt 0.4, you need to migrate it" << std::endl;
1034 std::clog << "by running 'git-crypt migrate-key /path/to/old_key /path/to/migrated_key'." << std::endl;
1035 return 1;
1036 }
1037
1038 key_files.push_back(key_file);
1039 }
1040 } else {
1041 // Decrypt GPG key from root of repo
1042 std::string repo_keys_path(get_repo_keys_path());
1043 std::vector<std::string> gpg_secret_keys(gpg_list_secret_keys());
1044 // TODO: command-line option to specify the precise secret key to use
1045 // 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
1046 // TODO: command line option to only unlock specific key instead of all of them
1047 // TODO: avoid decrypting repo keys which are already unlocked in the .git directory
1048 if (!decrypt_repo_keys(key_files, 0, gpg_secret_keys, repo_keys_path)) {
1049 std::clog << "Error: no GPG secret key available to unlock this repository." << std::endl;
1050 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;
1051 // TODO std::clog << "To see a list of GPG keys authorized to unlock this repository, run 'git-crypt ls-gpg-users'." << std::endl;
1052 return 1;
1053 }
1054 }
1055
1056
1057 // 3. Install the key(s) and configure the git filters
1058 std::vector<std::string> encrypted_files;
1059 for (std::vector<Key_file>::iterator key_file(key_files.begin()); key_file != key_files.end(); ++key_file) {
1060 std::string internal_key_path(get_internal_key_path(key_file->get_key_name()));
1061 // TODO: croak if internal_key_path already exists???
1062 mkdir_parent(internal_key_path);
1063 if (!key_file->store_to_file(internal_key_path.c_str())) {
1064 std::clog << "Error: " << internal_key_path << ": unable to write key file" << std::endl;
1065 return 1;
1066 }
1067
1068 configure_git_filters(key_file->get_key_name());
1069 get_encrypted_files(encrypted_files, key_file->get_key_name());
1070 }
1071
1072 // 4. Check out the files that are currently encrypted.
1073 // Git won't check out a file if its mtime hasn't changed, so touch every file first.
1074 for (std::vector<std::string>::const_iterator file(encrypted_files.begin()); file != encrypted_files.end(); ++file) {
1075 touch_file(*file);
1076 }
1077 if (!git_checkout(encrypted_files)) {
1078 std::clog << "Error: 'git checkout' failed" << std::endl;
1079 std::clog << "git-crypt has been set up but existing encrypted files have not been decrypted" << std::endl;
1080 return 1;
1081 }
1082
1083 return 0;
1084 }
1085
help_lock(std::ostream & out)1086 void help_lock (std::ostream& out)
1087 {
1088 // |--------------------------------------------------------------------------------| 80 chars
1089 out << "Usage: git-crypt lock [OPTIONS]" << std::endl;
1090 out << std::endl;
1091 out << " -a, --all Lock all keys, instead of just the default" << std::endl;
1092 out << " -k, --key-name KEYNAME Lock the given key, instead of the default" << std::endl;
1093 out << " -f, --force Lock even if unclean (you may lose uncommited work)" << std::endl;
1094 out << std::endl;
1095 }
lock(int argc,const char ** argv)1096 int lock (int argc, const char** argv)
1097 {
1098 const char* key_name = 0;
1099 bool all_keys = false;
1100 bool force = false;
1101 Options_list options;
1102 options.push_back(Option_def("-k", &key_name));
1103 options.push_back(Option_def("--key-name", &key_name));
1104 options.push_back(Option_def("-a", &all_keys));
1105 options.push_back(Option_def("--all", &all_keys));
1106 options.push_back(Option_def("-f", &force));
1107 options.push_back(Option_def("--force", &force));
1108
1109 int argi = parse_options(options, argc, argv);
1110
1111 if (argc - argi != 0) {
1112 std::clog << "Error: git-crypt lock takes no arguments" << std::endl;
1113 help_lock(std::clog);
1114 return 2;
1115 }
1116
1117 if (all_keys && key_name) {
1118 std::clog << "Error: -k and --all options are mutually exclusive" << std::endl;
1119 return 2;
1120 }
1121
1122 // 1. Make sure working directory is clean (ignoring untracked files)
1123 // We do this because we check out files later, and we don't want the
1124 // user to lose any changes. (TODO: only care if encrypted files are
1125 // modified, since we only check out encrypted files)
1126
1127 // Running 'git status' also serves as a check that the Git repo is accessible.
1128
1129 std::stringstream status_output;
1130 get_git_status(status_output);
1131 if (!force && status_output.peek() != -1) {
1132 std::clog << "Error: Working directory not clean." << std::endl;
1133 std::clog << "Please commit your changes or 'git stash' them before running 'git-crypt lock'." << std::endl;
1134 std::clog << "Or, use 'git-crypt lock --force' and possibly lose uncommitted changes." << std::endl;
1135 return 1;
1136 }
1137
1138 // 2. deconfigure the git filters and remove decrypted keys
1139 std::vector<std::string> encrypted_files;
1140 if (all_keys) {
1141 // deconfigure for all keys
1142 std::vector<std::string> dirents = get_directory_contents(get_internal_keys_path().c_str());
1143
1144 for (std::vector<std::string>::const_iterator dirent(dirents.begin()); dirent != dirents.end(); ++dirent) {
1145 const char* this_key_name = (*dirent == "default" ? 0 : dirent->c_str());
1146 remove_file(get_internal_key_path(this_key_name));
1147 deconfigure_git_filters(this_key_name);
1148 get_encrypted_files(encrypted_files, this_key_name);
1149 }
1150 } else {
1151 // just handle the given key
1152 std::string internal_key_path(get_internal_key_path(key_name));
1153 if (access(internal_key_path.c_str(), F_OK) == -1 && errno == ENOENT) {
1154 std::clog << "Error: this repository is already locked";
1155 if (key_name) {
1156 std::clog << " with key '" << key_name << "'";
1157 }
1158 std::clog << "." << std::endl;
1159 return 1;
1160 }
1161
1162 remove_file(internal_key_path);
1163 deconfigure_git_filters(key_name);
1164 get_encrypted_files(encrypted_files, key_name);
1165 }
1166
1167 // 3. Check out the files that are currently decrypted but should be encrypted.
1168 // Git won't check out a file if its mtime hasn't changed, so touch every file first.
1169 for (std::vector<std::string>::const_iterator file(encrypted_files.begin()); file != encrypted_files.end(); ++file) {
1170 touch_file(*file);
1171 }
1172 if (!git_checkout(encrypted_files)) {
1173 std::clog << "Error: 'git checkout' failed" << std::endl;
1174 std::clog << "git-crypt has been locked but up but existing decrypted files have not been encrypted" << std::endl;
1175 return 1;
1176 }
1177
1178 return 0;
1179 }
1180
help_add_gpg_user(std::ostream & out)1181 void help_add_gpg_user (std::ostream& out)
1182 {
1183 // |--------------------------------------------------------------------------------| 80 chars
1184 out << "Usage: git-crypt add-gpg-user [OPTIONS] GPG_USER_ID ..." << std::endl;
1185 out << std::endl;
1186 out << " -k, --key-name KEYNAME Add GPG user to given key, instead of default" << std::endl;
1187 out << " -n, --no-commit Don't automatically commit" << std::endl;
1188 out << " --trusted Assume the GPG user IDs are trusted" << std::endl;
1189 out << std::endl;
1190 }
add_gpg_user(int argc,const char ** argv)1191 int add_gpg_user (int argc, const char** argv)
1192 {
1193 const char* key_name = 0;
1194 bool no_commit = false;
1195 bool trusted = false;
1196 Options_list options;
1197 options.push_back(Option_def("-k", &key_name));
1198 options.push_back(Option_def("--key-name", &key_name));
1199 options.push_back(Option_def("-n", &no_commit));
1200 options.push_back(Option_def("--no-commit", &no_commit));
1201 options.push_back(Option_def("--trusted", &trusted));
1202
1203 int argi = parse_options(options, argc, argv);
1204 if (argc - argi == 0) {
1205 std::clog << "Error: no GPG user ID specified" << std::endl;
1206 help_add_gpg_user(std::clog);
1207 return 2;
1208 }
1209
1210 // build a list of key fingerprints, and whether the key is trusted, for every collaborator specified on the command line
1211 std::vector<std::pair<std::string, bool> > collab_keys;
1212
1213 for (int i = argi; i < argc; ++i) {
1214 std::vector<std::string> keys(gpg_lookup_key(argv[i]));
1215 if (keys.empty()) {
1216 std::clog << "Error: public key for '" << argv[i] << "' not found in your GPG keyring" << std::endl;
1217 return 1;
1218 }
1219 if (keys.size() > 1) {
1220 std::clog << "Error: more than one public key matches '" << argv[i] << "' - please be more specific" << std::endl;
1221 return 1;
1222 }
1223
1224 const bool is_full_fingerprint(std::strncmp(argv[i], "0x", 2) == 0 && std::strlen(argv[i]) == 42);
1225 collab_keys.push_back(std::make_pair(keys[0], trusted || is_full_fingerprint));
1226 }
1227
1228 // TODO: have a retroactive option to grant access to all key versions, not just the most recent
1229 Key_file key_file;
1230 load_key(key_file, key_name);
1231 const Key_file::Entry* key = key_file.get_latest();
1232 if (!key) {
1233 std::clog << "Error: key file is empty" << std::endl;
1234 return 1;
1235 }
1236
1237 const std::string state_path(get_repo_state_path());
1238 std::vector<std::string> new_files;
1239
1240 encrypt_repo_key(key_name, *key, collab_keys, get_repo_keys_path(state_path), &new_files);
1241
1242 // Add a .gitatributes file to the repo state directory to prevent files in it from being encrypted.
1243 const std::string state_gitattributes_path(state_path + "/.gitattributes");
1244 if (access(state_gitattributes_path.c_str(), F_OK) != 0) {
1245 std::ofstream state_gitattributes_file(state_gitattributes_path.c_str());
1246 // |--------------------------------------------------------------------------------| 80 chars
1247 state_gitattributes_file << "# Do not edit this file. To specify the files to encrypt, create your own\n";
1248 state_gitattributes_file << "# .gitattributes file in the directory where your files are.\n";
1249 state_gitattributes_file << "* !filter !diff\n";
1250 state_gitattributes_file << "*.gpg binary\n";
1251 state_gitattributes_file.close();
1252 if (!state_gitattributes_file) {
1253 std::clog << "Error: unable to write " << state_gitattributes_path << std::endl;
1254 return 1;
1255 }
1256 new_files.push_back(state_gitattributes_path);
1257 }
1258
1259 // add/commit the new files
1260 if (!new_files.empty()) {
1261 // git add NEW_FILE ...
1262 std::vector<std::string> command;
1263 command.push_back("/usr/local/bin/git");
1264 command.push_back("add");
1265 command.push_back("--");
1266 command.insert(command.end(), new_files.begin(), new_files.end());
1267 if (!successful_exit(exec_command(command))) {
1268 std::clog << "Error: 'git add' failed" << std::endl;
1269 return 1;
1270 }
1271
1272 // git commit ...
1273 if (!no_commit) {
1274 // TODO: include key_name in commit message
1275 std::ostringstream commit_message_builder;
1276 commit_message_builder << "Add " << collab_keys.size() << " git-crypt collaborator" << (collab_keys.size() != 1 ? "s" : "") << "\n\nNew collaborators:\n\n";
1277 for (std::vector<std::pair<std::string, bool> >::const_iterator collab(collab_keys.begin()); collab != collab_keys.end(); ++collab) {
1278 commit_message_builder << '\t' << gpg_shorten_fingerprint(collab->first) << ' ' << gpg_get_uid(collab->first) << '\n';
1279 }
1280
1281 // git commit -m MESSAGE NEW_FILE ...
1282 command.clear();
1283 command.push_back("/usr/local/bin/git");
1284 command.push_back("commit");
1285 command.push_back("-m");
1286 command.push_back(commit_message_builder.str());
1287 command.push_back("--");
1288 command.insert(command.end(), new_files.begin(), new_files.end());
1289
1290 if (!successful_exit(exec_command(command))) {
1291 std::clog << "Error: 'git commit' failed" << std::endl;
1292 return 1;
1293 }
1294 }
1295 }
1296
1297 return 0;
1298 }
1299
help_rm_gpg_user(std::ostream & out)1300 void help_rm_gpg_user (std::ostream& out)
1301 {
1302 // |--------------------------------------------------------------------------------| 80 chars
1303 out << "Usage: git-crypt rm-gpg-user [OPTIONS] GPG_USER_ID ..." << std::endl;
1304 out << std::endl;
1305 out << " -k, --key-name KEYNAME Remove user from given key, instead of default" << std::endl;
1306 out << " -n, --no-commit Don't automatically commit" << std::endl;
1307 out << std::endl;
1308 }
rm_gpg_user(int argc,const char ** argv)1309 int rm_gpg_user (int argc, const char** argv) // TODO
1310 {
1311 std::clog << "Error: rm-gpg-user is not yet implemented." << std::endl;
1312 return 1;
1313 }
1314
help_ls_gpg_users(std::ostream & out)1315 void help_ls_gpg_users (std::ostream& out)
1316 {
1317 // |--------------------------------------------------------------------------------| 80 chars
1318 out << "Usage: git-crypt ls-gpg-users" << std::endl;
1319 }
ls_gpg_users(int argc,const char ** argv)1320 int ls_gpg_users (int argc, const char** argv) // TODO
1321 {
1322 // Sketch:
1323 // Scan the sub-directories in .git-crypt/keys, outputting something like this:
1324 // ====
1325 // Key version 0:
1326 // 0x143DE9B3F7316900 Andrew Ayer <andrew@example.com>
1327 // 0x4E386D9C9C61702F ???
1328 // Key version 1:
1329 // 0x143DE9B3F7316900 Andrew Ayer <andrew@example.com>
1330 // 0x1727274463D27F40 John Smith <smith@example.com>
1331 // 0x4E386D9C9C61702F ???
1332 // ====
1333 // To resolve a long hex ID, use a command like this:
1334 // gpg --options /dev/null --fixed-list-mode --batch --with-colons --list-keys 0x143DE9B3F7316900
1335
1336 std::clog << "Error: ls-gpg-users is not yet implemented." << std::endl;
1337 return 1;
1338 }
1339
help_export_key(std::ostream & out)1340 void help_export_key (std::ostream& out)
1341 {
1342 // |--------------------------------------------------------------------------------| 80 chars
1343 out << "Usage: git-crypt export-key [OPTIONS] FILENAME" << std::endl;
1344 out << std::endl;
1345 out << " -k, --key-name KEYNAME Export the given key, instead of the default" << std::endl;
1346 out << std::endl;
1347 out << "When FILENAME is -, export to standard out." << std::endl;
1348 }
export_key(int argc,const char ** argv)1349 int export_key (int argc, const char** argv)
1350 {
1351 // TODO: provide options to export only certain key versions
1352 const char* key_name = 0;
1353 Options_list options;
1354 options.push_back(Option_def("-k", &key_name));
1355 options.push_back(Option_def("--key-name", &key_name));
1356
1357 int argi = parse_options(options, argc, argv);
1358
1359 if (argc - argi != 1) {
1360 std::clog << "Error: no filename specified" << std::endl;
1361 help_export_key(std::clog);
1362 return 2;
1363 }
1364
1365 Key_file key_file;
1366 load_key(key_file, key_name);
1367
1368 const char* out_file_name = argv[argi];
1369
1370 if (std::strcmp(out_file_name, "-") == 0) {
1371 key_file.store(std::cout);
1372 } else {
1373 if (!key_file.store_to_file(out_file_name)) {
1374 std::clog << "Error: " << out_file_name << ": unable to write key file" << std::endl;
1375 return 1;
1376 }
1377 }
1378
1379 return 0;
1380 }
1381
help_keygen(std::ostream & out)1382 void help_keygen (std::ostream& out)
1383 {
1384 // |--------------------------------------------------------------------------------| 80 chars
1385 out << "Usage: git-crypt keygen FILENAME" << std::endl;
1386 out << std::endl;
1387 out << "When FILENAME is -, write to standard out." << std::endl;
1388 }
keygen(int argc,const char ** argv)1389 int keygen (int argc, const char** argv)
1390 {
1391 if (argc != 1) {
1392 std::clog << "Error: no filename specified" << std::endl;
1393 help_keygen(std::clog);
1394 return 2;
1395 }
1396
1397 const char* key_file_name = argv[0];
1398
1399 if (std::strcmp(key_file_name, "-") != 0 && access(key_file_name, F_OK) == 0) {
1400 std::clog << key_file_name << ": File already exists" << std::endl;
1401 return 1;
1402 }
1403
1404 std::clog << "Generating key..." << std::endl;
1405 Key_file key_file;
1406 key_file.generate();
1407
1408 if (std::strcmp(key_file_name, "-") == 0) {
1409 key_file.store(std::cout);
1410 } else {
1411 if (!key_file.store_to_file(key_file_name)) {
1412 std::clog << "Error: " << key_file_name << ": unable to write key file" << std::endl;
1413 return 1;
1414 }
1415 }
1416 return 0;
1417 }
1418
help_migrate_key(std::ostream & out)1419 void help_migrate_key (std::ostream& out)
1420 {
1421 // |--------------------------------------------------------------------------------| 80 chars
1422 out << "Usage: git-crypt migrate-key OLDFILENAME NEWFILENAME" << std::endl;
1423 out << std::endl;
1424 out << "Use - to read from standard in/write to standard out." << std::endl;
1425 }
migrate_key(int argc,const char ** argv)1426 int migrate_key (int argc, const char** argv)
1427 {
1428 if (argc != 2) {
1429 std::clog << "Error: filenames not specified" << std::endl;
1430 help_migrate_key(std::clog);
1431 return 2;
1432 }
1433
1434 const char* key_file_name = argv[0];
1435 const char* new_key_file_name = argv[1];
1436 Key_file key_file;
1437
1438 try {
1439 if (std::strcmp(key_file_name, "-") == 0) {
1440 key_file.load_legacy(std::cin);
1441 } else {
1442 std::ifstream in(key_file_name, std::fstream::binary);
1443 if (!in) {
1444 std::clog << "Error: " << key_file_name << ": unable to open for reading" << std::endl;
1445 return 1;
1446 }
1447 key_file.load_legacy(in);
1448 }
1449
1450 if (std::strcmp(new_key_file_name, "-") == 0) {
1451 key_file.store(std::cout);
1452 } else {
1453 if (!key_file.store_to_file(new_key_file_name)) {
1454 std::clog << "Error: " << new_key_file_name << ": unable to write key file" << std::endl;
1455 return 1;
1456 }
1457 }
1458 } catch (Key_file::Malformed) {
1459 std::clog << "Error: " << key_file_name << ": not a valid legacy git-crypt key file" << std::endl;
1460 return 1;
1461 }
1462
1463 return 0;
1464 }
1465
help_refresh(std::ostream & out)1466 void help_refresh (std::ostream& out)
1467 {
1468 // |--------------------------------------------------------------------------------| 80 chars
1469 out << "Usage: git-crypt refresh" << std::endl;
1470 }
refresh(int argc,const char ** argv)1471 int refresh (int argc, const char** argv) // TODO: do a force checkout, much like in unlock
1472 {
1473 std::clog << "Error: refresh is not yet implemented." << std::endl;
1474 return 1;
1475 }
1476
help_status(std::ostream & out)1477 void help_status (std::ostream& out)
1478 {
1479 // |--------------------------------------------------------------------------------| 80 chars
1480 out << "Usage: git-crypt status [OPTIONS] [FILE ...]" << std::endl;
1481 //out << " or: git-crypt status -r [OPTIONS]" << std::endl;
1482 //out << " or: git-crypt status -f" << std::endl;
1483 out << std::endl;
1484 out << " -e Show encrypted files only" << std::endl;
1485 out << " -u Show unencrypted files only" << std::endl;
1486 //out << " -r Show repository status only" << std::endl;
1487 out << " -f, --fix Fix problems with the repository" << std::endl;
1488 //out << " -z Machine-parseable output" << std::endl;
1489 out << std::endl;
1490 }
status(int argc,const char ** argv)1491 int status (int argc, const char** argv)
1492 {
1493 // Usage:
1494 // git-crypt status -r [-z] Show repo status
1495 // git-crypt status [-e | -u] [-z] [FILE ...] Show encrypted status of files
1496 // git-crypt status -f Fix unencrypted blobs
1497
1498 bool repo_status_only = false; // -r show repo status only
1499 bool show_encrypted_only = false; // -e show encrypted files only
1500 bool show_unencrypted_only = false; // -u show unencrypted files only
1501 bool fix_problems = false; // -f fix problems
1502 bool machine_output = false; // -z machine-parseable output
1503
1504 Options_list options;
1505 options.push_back(Option_def("-r", &repo_status_only));
1506 options.push_back(Option_def("-e", &show_encrypted_only));
1507 options.push_back(Option_def("-u", &show_unencrypted_only));
1508 options.push_back(Option_def("-f", &fix_problems));
1509 options.push_back(Option_def("--fix", &fix_problems));
1510 options.push_back(Option_def("-z", &machine_output));
1511
1512 int argi = parse_options(options, argc, argv);
1513
1514 if (repo_status_only) {
1515 if (show_encrypted_only || show_unencrypted_only) {
1516 std::clog << "Error: -e and -u options cannot be used with -r" << std::endl;
1517 return 2;
1518 }
1519 if (fix_problems) {
1520 std::clog << "Error: -f option cannot be used with -r" << std::endl;
1521 return 2;
1522 }
1523 if (argc - argi != 0) {
1524 std::clog << "Error: filenames cannot be specified when -r is used" << std::endl;
1525 return 2;
1526 }
1527 }
1528
1529 if (show_encrypted_only && show_unencrypted_only) {
1530 std::clog << "Error: -e and -u options are mutually exclusive" << std::endl;
1531 return 2;
1532 }
1533
1534 if (fix_problems && (show_encrypted_only || show_unencrypted_only)) {
1535 std::clog << "Error: -e and -u options cannot be used with -f" << std::endl;
1536 return 2;
1537 }
1538
1539 if (machine_output) {
1540 // TODO: implement machine-parseable output
1541 std::clog << "Sorry, machine-parseable output is not yet implemented" << std::endl;
1542 return 2;
1543 }
1544
1545 if (argc - argi == 0) {
1546 // TODO: check repo status:
1547 // is it set up for git-crypt?
1548 // which keys are unlocked?
1549 // --> check for filter config (see configure_git_filters()) and corresponding internal key
1550
1551 if (repo_status_only) {
1552 return 0;
1553 }
1554 }
1555
1556 // git ls-files -cotsz --exclude-standard ...
1557 std::vector<std::string> command;
1558 command.push_back("/usr/local/bin/git");
1559 command.push_back("ls-files");
1560 command.push_back("-cotsz");
1561 command.push_back("--exclude-standard");
1562 command.push_back("--");
1563 if (argc - argi == 0) {
1564 const std::string path_to_top(get_path_to_top());
1565 if (!path_to_top.empty()) {
1566 command.push_back(path_to_top);
1567 }
1568 } else {
1569 for (int i = argi; i < argc; ++i) {
1570 command.push_back(argv[i]);
1571 }
1572 }
1573
1574 std::stringstream output;
1575 if (!successful_exit(exec_command(command, output))) {
1576 throw Error("'git ls-files' failed - is this a Git repository?");
1577 }
1578
1579 // Output looks like (w/o newlines):
1580 // ? .gitignore\0
1581 // H 100644 06ec22e5ed0de9280731ef000a10f9c3fbc26338 0 afile\0
1582
1583 std::vector<std::string> files;
1584 bool attribute_errors = false;
1585 bool unencrypted_blob_errors = false;
1586 unsigned int nbr_of_fixed_blobs = 0;
1587 unsigned int nbr_of_fix_errors = 0;
1588
1589 while (output.peek() != -1) {
1590 std::string tag;
1591 std::string object_id;
1592 std::string filename;
1593 output >> tag;
1594 if (tag != "?") {
1595 std::string mode;
1596 std::string stage;
1597 output >> mode >> object_id >> stage;
1598 if (!is_git_file_mode(mode)) {
1599 continue;
1600 }
1601 }
1602 output >> std::ws;
1603 std::getline(output, filename, '\0');
1604
1605 // 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)
1606 const std::pair<std::string, std::string> file_attrs(get_file_attributes(filename));
1607
1608 if (file_attrs.first == "git-crypt" || std::strncmp(file_attrs.first.c_str(), "git-crypt-", 10) == 0) {
1609 // File is encrypted
1610 const bool blob_is_unencrypted = !object_id.empty() && !check_if_blob_is_encrypted(object_id);
1611
1612 if (fix_problems && blob_is_unencrypted) {
1613 if (access(filename.c_str(), F_OK) != 0) {
1614 std::clog << "Error: " << filename << ": cannot stage encrypted version because not present in working tree - please 'git rm' or 'git checkout' it" << std::endl;
1615 ++nbr_of_fix_errors;
1616 } else {
1617 touch_file(filename);
1618 std::vector<std::string> git_add_command;
1619 git_add_command.push_back("/usr/local/bin/git");
1620 git_add_command.push_back("add");
1621 git_add_command.push_back("--");
1622 git_add_command.push_back(filename);
1623 if (!successful_exit(exec_command(git_add_command))) {
1624 throw Error("'git-add' failed");
1625 }
1626 if (check_if_file_is_encrypted(filename)) {
1627 std::cout << filename << ": staged encrypted version" << std::endl;
1628 ++nbr_of_fixed_blobs;
1629 } else {
1630 std::clog << "Error: " << filename << ": still unencrypted even after staging" << std::endl;
1631 ++nbr_of_fix_errors;
1632 }
1633 }
1634 } else if (!fix_problems && !show_unencrypted_only) {
1635 // TODO: output the key name used to encrypt this file
1636 std::cout << " encrypted: " << filename;
1637 if (file_attrs.second != file_attrs.first) {
1638 // but diff filter is not properly set
1639 std::cout << " *** WARNING: diff=" << file_attrs.first << " attribute not set ***";
1640 attribute_errors = true;
1641 }
1642 if (blob_is_unencrypted) {
1643 // File not actually encrypted
1644 std::cout << " *** WARNING: staged/committed version is NOT ENCRYPTED! ***";
1645 unencrypted_blob_errors = true;
1646 }
1647 std::cout << std::endl;
1648 }
1649 } else {
1650 // File not encrypted
1651 if (!fix_problems && !show_encrypted_only) {
1652 std::cout << "not encrypted: " << filename << std::endl;
1653 }
1654 }
1655 }
1656
1657 int exit_status = 0;
1658
1659 if (attribute_errors) {
1660 std::cout << std::endl;
1661 std::cout << "Warning: one or more files has a git-crypt filter attribute but not a" << std::endl;
1662 std::cout << "corresponding git-crypt diff attribute. For proper 'git diff' operation" << std::endl;
1663 std::cout << "you should fix the .gitattributes file to specify the correct diff attribute." << std::endl;
1664 std::cout << "Consult the git-crypt documentation for help." << std::endl;
1665 exit_status = 1;
1666 }
1667 if (unencrypted_blob_errors) {
1668 std::cout << std::endl;
1669 std::cout << "Warning: one or more files is marked for encryption via .gitattributes but" << std::endl;
1670 std::cout << "was staged and/or committed before the .gitattributes file was in effect." << std::endl;
1671 std::cout << "Run 'git-crypt status' with the '-f' option to stage an encrypted version." << std::endl;
1672 exit_status = 1;
1673 }
1674 if (nbr_of_fixed_blobs) {
1675 std::cout << "Staged " << nbr_of_fixed_blobs << " encrypted file" << (nbr_of_fixed_blobs != 1 ? "s" : "") << "." << std::endl;
1676 std::cout << "Warning: if these files were previously committed, unencrypted versions still exist in the repository's history." << std::endl;
1677 }
1678 if (nbr_of_fix_errors) {
1679 std::cout << "Unable to stage " << nbr_of_fix_errors << " file" << (nbr_of_fix_errors != 1 ? "s" : "") << "." << std::endl;
1680 exit_status = 1;
1681 }
1682
1683 return exit_status;
1684 }
1685
1686