aboutsummaryrefslogtreecommitdiffstats
path: root/community/git-crypt/0001-add-merge-driver.patch
blob: 771cc56e333441576ead2353447e7839a2706158 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
From b6f4f33ba969cfd44cdd9a5df2048ad5b6330fb0 Mon Sep 17 00:00:00 2001
From: Jakub Jirutka <jakub@jirutka.cz>
Date: Sun, 28 Jul 2019 17:14:09 +0200
Subject: [PATCH 1/2] Change clean() and smudge() functions to accept in/out
 stream

This is a preparation for the merge command.

I've extracted this change from #107.

Co-Authored-By: Shlomo Shachar <shlomo.shachar@binatix.com>
Patch-Source: https://github.com/AGWA/git-crypt/pull/180
---
 commands.cpp | 38 +++++++++++++++++++-------------------
 commands.hpp |  5 +++--
 2 files changed, 22 insertions(+), 21 deletions(-)

diff --git a/commands.cpp b/commands.cpp
index d25c4cc..93a840e 100644
--- a/commands.cpp
+++ b/commands.cpp
@@ -690,8 +690,8 @@ static int parse_plumbing_options (const char** key_name, const char** key_file,
 	return parse_options(options, argc, argv);
 }
 
-// Encrypt contents of stdin and write to stdout
-int clean (int argc, const char** argv)
+// Encrypt contents of &in and write to &out
+int clean (int argc, const char** argv, std::istream& in, std::ostream& out)
 {
 	const char*		key_name = 0;
 	const char*		key_path = 0;
@@ -724,10 +724,10 @@ int clean (int argc, const char** argv)
 
 	char			buffer[1024];
 
-	while (std::cin && file_size < Aes_ctr_encryptor::MAX_CRYPT_BYTES) {
-		std::cin.read(buffer, sizeof(buffer));
+	while (in && file_size < Aes_ctr_encryptor::MAX_CRYPT_BYTES) {
+		in.read(buffer, sizeof(buffer));
 
-		const size_t	bytes_read = std::cin.gcount();
+		const size_t	bytes_read = in.gcount();
 
 		hmac.add(reinterpret_cast<unsigned char*>(buffer), bytes_read);
 		file_size += bytes_read;
@@ -775,8 +775,8 @@ int clean (int argc, const char** argv)
 	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
+	out.write("\0GITCRYPT\0", 10); // ...identifies this as an encrypted file
+	out.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);
@@ -787,7 +787,7 @@ int clean (int argc, const char** argv)
 	while (file_data_len > 0) {
 		const 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);
+		out.write(buffer, buffer_len);
 		file_data += buffer_len;
 		file_data_len -= buffer_len;
 	}
@@ -803,14 +803,14 @@ int clean (int argc, const char** argv)
 			aes.process(reinterpret_cast<unsigned char*>(buffer),
 			            reinterpret_cast<unsigned char*>(buffer),
 			            buffer_len);
-			std::cout.write(buffer, buffer_len);
+			out.write(buffer, buffer_len);
 		}
 	}
 
 	return 0;
 }
 
-static int decrypt_file_to_stdout (const Key_file& key_file, const unsigned char* header, std::istream& in)
+static int decrypt_file_to_stream (const Key_file& key_file, const unsigned char* header, std::istream& in, std::ostream& out = std::cout)
 {
 	const unsigned char*	nonce = header + 10;
 	uint32_t		key_version = 0; // TODO: get the version from the file header
@@ -828,7 +828,7 @@ static int decrypt_file_to_stdout (const Key_file& key_file, const unsigned char
 		in.read(reinterpret_cast<char*>(buffer), sizeof(buffer));
 		aes.process(buffer, buffer, in.gcount());
 		hmac.add(buffer, in.gcount());
-		std::cout.write(reinterpret_cast<char*>(buffer), in.gcount());
+		out.write(reinterpret_cast<char*>(buffer), in.gcount());
 	}
 
 	unsigned char		digest[Hmac_sha1_state::LEN];
@@ -844,8 +844,8 @@ static int decrypt_file_to_stdout (const Key_file& key_file, const unsigned char
 	return 0;
 }
 
-// Decrypt contents of stdin and write to stdout
-int smudge (int argc, const char** argv)
+// Decrypt contents of &in and write to &out
+int smudge (int argc, const char** argv, std::istream& in, std::ostream& out)
 {
 	const char*		key_name = 0;
 	const char*		key_path = 0;
@@ -864,8 +864,8 @@ int smudge (int argc, const char** argv)
 
 	// 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.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) {
+	in.read(reinterpret_cast<char*>(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::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;
@@ -873,12 +873,12 @@ int smudge (int argc, const char** argv)
 		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<char*>(header), std::cin.gcount()); // include the bytes which we already read
-		std::cout << std::cin.rdbuf();
+		out.write(reinterpret_cast<char*>(header), in.gcount()); // include the bytes which we already read
+		out << in.rdbuf();
 		return 0;
 	}
 
-	return decrypt_file_to_stdout(key_file, header, std::cin);
+	return decrypt_file_to_stream(key_file, header, in, out);
 }
 
 int diff (int argc, const char** argv)
@@ -920,7 +920,7 @@ int diff (int argc, const char** argv)
 	}
 
 	// Go ahead and decrypt it
-	return decrypt_file_to_stdout(key_file, header, in);
+	return decrypt_file_to_stream(key_file, header, in);
 }
 
 void help_init (std::ostream& out)
diff --git a/commands.hpp b/commands.hpp
index f441e93..bf4632c 100644
--- a/commands.hpp
+++ b/commands.hpp
@@ -33,6 +33,7 @@
 
 #include <string>
 #include <iosfwd>
+#include <iostream>
 
 struct Error {
 	std::string	message;
@@ -41,8 +42,8 @@ struct Error {
 };
 
 // Plumbing commands:
-int clean (int argc, const char** argv);
-int smudge (int argc, const char** argv);
+int clean (int argc, const char** argv, std::istream& in = std::cin, std::ostream& out = std::cout);
+int smudge (int argc, const char** argv, std::istream& in = std::cin, std::ostream& out = std::cout);
 int diff (int argc, const char** argv);
 // Public commands:
 int init (int argc, const char** argv);

From b2efa705c4bd8c5d5590816226aace123bf19265 Mon Sep 17 00:00:00 2001
From: Jakub Jirutka <jakub@jirutka.cz>
Date: Sun, 28 Jul 2019 19:18:39 +0200
Subject: [PATCH 2/2] Add git-crypt merge driver to support secret files
 merging

This commit is based on #107.

Co-Authored-By: Shlomo Shachar <shlomo.shachar@binatix.com>
Patch-Source: https://github.com/AGWA/git-crypt/pull/180
---
 README               |   8 ++--
 README.md            |   8 ++--
 commands.cpp         | 111 +++++++++++++++++++++++++++++++++++++++++++
 commands.hpp         |   1 +
 doc/multiple_keys.md |   2 +-
 git-crypt.cpp        |   4 ++
 man/git-crypt.xml    |   8 ++--
 7 files changed, 129 insertions(+), 13 deletions(-)

diff --git a/README b/README
index 232947f..2810a26 100644
--- a/README
+++ b/README
@@ -28,8 +28,8 @@ Configure a repository to use git-crypt:
 
 Specify files to encrypt by creating a .gitattributes file:
 
-	secretfile filter=git-crypt diff=git-crypt
-	*.key filter=git-crypt diff=git-crypt
+	secretfile filter=git-crypt diff=git-crypt merge=git-crypt
+	*.key filter=git-crypt diff=git-crypt merge=git-crypt
 
 Like a .gitignore file, it can match wildcards and should be checked into
 the repository.  See below for more information about .gitattributes.
@@ -151,8 +151,8 @@ 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
+	* filter=git-crypt diff=git-crypt merge=git-crypt
+	.gitattributes !filter !diff !merge
 
 The second pattern is essential for ensuring that .gitattributes itself
 is not encrypted.
diff --git a/README.md b/README.md
index d24517a..d008aab 100644
--- a/README.md
+++ b/README.md
@@ -29,8 +29,8 @@ Configure a repository to use git-crypt:
 
 Specify files to encrypt by creating a .gitattributes file:
 
-    secretfile filter=git-crypt diff=git-crypt
-    *.key filter=git-crypt diff=git-crypt
+    secretfile filter=git-crypt diff=git-crypt merge=git-crypt
+    *.key filter=git-crypt diff=git-crypt merge=git-crypt
 
 Like a .gitignore file, it can match wildcards and should be checked into
 the repository.  See below for more information about .gitattributes.
@@ -153,8 +153,8 @@ 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
+    * filter=git-crypt diff=git-crypt merge=git-crypt
+    .gitattributes !filter !diff !merge
 
 The second pattern is essential for ensuring that .gitattributes itself
 is not encrypted.
diff --git a/commands.cpp b/commands.cpp
index 93a840e..415b36a 100644
--- a/commands.cpp
+++ b/commands.cpp
@@ -160,11 +160,16 @@ static void configure_git_filters (const char* 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);
+		git_config(std::string("merge.git-crypt-") + key_name + ".name", "git-crypt merge driver");
+		git_config(std::string("merge.git-crypt-") + key_name + ".driver",
+		           escaped_git_crypt_path + " merge --key-name=" + key_name + " %A %O %B %L");
 	} 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");
+		git_config("merge.git-crypt.name", "git-crypt merge driver");
+		git_config("merge.git-crypt.driver", escaped_git_crypt_path + " merge %A %O %B %L");
 	}
 }
 
@@ -181,6 +186,12 @@ static void deconfigure_git_filters (const char* key_name)
 	if (git_has_config("diff." + attribute_name(key_name) + ".textconv")) {
 		git_deconfig("diff." + attribute_name(key_name));
 	}
+
+	if (git_has_config("merge." + attribute_name(key_name) + ".name") ||
+			git_has_config("merge." + attribute_name(key_name) + ".driver")) {
+
+		git_deconfig("merge." + attribute_name(key_name));
+	}
 }
 
 static bool git_checkout (const std::vector<std::string>& paths)
@@ -923,6 +934,106 @@ int diff (int argc, const char** argv)
 	return decrypt_file_to_stream(key_file, header, in);
 }
 
+int merge (int argc, const char** argv)
+{
+	const char*		key_name = 0;     // unused but needed
+	const char*		key_path = 0;     // unused but needed
+	const char*		current_path = 0; // %A
+	const char*		base_path = 0;    // %O
+	const char*		other_path = 0;   // %B
+	const char*		marker_size = 0;  // %L
+
+	int			argi = parse_plumbing_options(&key_name, &key_path, argc, argv);
+	if (argc - argi == 4) {
+		current_path = argv[argi];
+		base_path = argv[argi + 1];
+		other_path = argv[argi + 2];
+		marker_size = argv[argi + 3];
+	} else {
+		std::clog << "Usage: git-crypt merge [--key-name=NAME] [--key-file=PATH] CURRENT BASE OTHER MARKER_SIZE" << std::endl;
+		return 2;
+	}
+
+	// Run smudge on input files
+	std::vector<std::string>	smudge_files;
+	smudge_files.push_back(current_path);
+	smudge_files.push_back(base_path);
+	smudge_files.push_back(other_path);
+
+	for (std::vector<std::string>::const_iterator file(smudge_files.begin()); file != smudge_files.end(); ++file) {
+		std::ifstream	in(*file, std::ifstream::binary);
+		if (!in) {
+			std::clog << "git-crypt: " << *file << ": unable to open for reading" << std::endl;
+			return 1;
+		}
+		in.exceptions(std::ifstream::badbit);
+
+		std::ofstream	out(*file + ".tmp", std::ofstream::binary | std::ofstream::trunc);
+		if (!out) {
+			std::clog << "git-crypt: " << *file << ".tmp: unable to open for writing" << std::endl;
+			return 1;
+		}
+		out.exceptions(std::ifstream::badbit);
+
+		if (smudge(argi, argv, in, out) != 0) {
+			std::clog << "Error: failed to smudge " << *file << ": unable to merge file" << std::endl;
+			return 1;
+		}
+		in.close();
+		out.close();
+	}
+
+	// git merge-file --marker-size <marker_size> <current_path> <base_path> <other_path>
+	std::vector<std::string> command;
+	command.push_back("git");
+	command.push_back("merge-file");
+	command.push_back("-L");
+	command.push_back("ours");
+	command.push_back("-L");
+	command.push_back("base");
+	command.push_back("-L");
+	command.push_back("theirs");
+	command.push_back("--marker-size");
+	command.push_back(marker_size);
+	command.push_back(std::string(current_path) + ".tmp");
+	command.push_back(std::string(base_path) + ".tmp");
+	command.push_back(std::string(other_path) + ".tmp");
+	int ret = exit_status(exec_command(command));
+
+	// Run clean on output file
+	// We have to clean (encrypt) the output file because git runs smudge filter on it
+	// afterwards which would complain about the file not being encrypted.
+	{
+		std::ifstream	in(std::string(current_path) + ".tmp", std::ifstream::binary);
+		if (!in) {
+			std::clog << "git-crypt: " << current_path << ".tmp: unable to open for reading" << std::endl;
+			return 1;
+		}
+		in.exceptions(std::ifstream::badbit);
+
+		std::ofstream	out(current_path, std::ofstream::binary | std::ofstream::trunc);
+		if (!out) {
+			std::clog << "git-crypt: " << current_path << ": unable to open for writing" << std::endl;
+			return 1;
+		}
+		out.exceptions(std::ifstream::badbit);
+
+		if (clean(argi, argv, in, out) != 0) {
+			std::clog << "Error: failed to clean " << current_path << ": unable to merge file" << std::endl;
+			return 1;
+		}
+		in.close();
+		out.close();
+	}
+
+	// Clean-up temporary files
+	for (std::vector<std::string>::const_iterator file(smudge_files.begin()); file != smudge_files.end(); ++file) {
+		remove_file(*file + ".tmp");
+	}
+
+	return ret;
+}
+
 void help_init (std::ostream& out)
 {
 	//     |--------------------------------------------------------------------------------| 80 chars
diff --git a/commands.hpp b/commands.hpp
index bf4632c..51f4aea 100644
--- a/commands.hpp
+++ b/commands.hpp
@@ -45,6 +45,7 @@ struct Error {
 int clean (int argc, const char** argv, std::istream& in = std::cin, std::ostream& out = std::cout);
 int smudge (int argc, const char** argv, std::istream& in = std::cin, std::ostream& out = std::cout);
 int diff (int argc, const char** argv);
+int merge (int argc, const char** argv);
 // Public commands:
 int init (int argc, const char** argv);
 int unlock (int argc, const char** argv);
diff --git a/doc/multiple_keys.md b/doc/multiple_keys.md
index 6d7fc69..66b462a 100644
--- a/doc/multiple_keys.md
+++ b/doc/multiple_keys.md
@@ -11,7 +11,7 @@ option to `git-crypt init` as follows:
 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
+    secretfile filter=git-crypt-KEYNAME diff=git-crypt-KEYNAME merge=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`
diff --git a/git-crypt.cpp b/git-crypt.cpp
index 9505834..d8c2072 100644
--- a/git-crypt.cpp
+++ b/git-crypt.cpp
@@ -73,6 +73,7 @@ static void print_usage (std::ostream& out)
 	out << "   clean [LEGACY-KEYFILE]" << std::endl;
 	out << "   smudge [LEGACY-KEYFILE]" << std::endl;
 	out << "   diff [LEGACY-KEYFILE] FILE" << std::endl;
+	out << "   merge" << std::endl;
 	*/
 	out << std::endl;
 	out << "See 'git-crypt help COMMAND' for more information on a specific command." << std::endl;
@@ -231,6 +232,9 @@ try {
 		if (std::strcmp(command, "diff") == 0) {
 			return diff(argc, argv);
 		}
+		if (std::strcmp(command, "merge") == 0) {
+			return merge(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);
diff --git a/man/git-crypt.xml b/man/git-crypt.xml
index 96f53d7..21e1359 100644
--- a/man/git-crypt.xml
+++ b/man/git-crypt.xml
@@ -310,11 +310,11 @@
 		<para>
 			Then, you specify the files to encrypt by creating a
 			<citerefentry><refentrytitle>gitattributes</refentrytitle><manvolnum>5</manvolnum></citerefentry> file.
-			Each file which you want to encrypt should be assigned the "<literal>filter=git-crypt diff=git-crypt</literal>"
+			Each file which you want to encrypt should be assigned the "<literal>filter=git-crypt diff=git-crypt merge=git-crypt</literal>"
 			attributes.  For example:
 		</para>
 
-		<screen>secretfile filter=git-crypt diff=git-crypt&#10;*.key filter=git-crypt diff=git-crypt</screen>
+		<screen>secretfile filter=git-crypt diff=git-crypt merge=git-crypt&#10;*.key filter=git-crypt diff=git-crypt merge=git-crypt</screen>
 
 		<para>
 			Like a <filename>.gitignore</filename> file, <filename>.gitattributes</filename> files can match wildcards and
@@ -383,7 +383,7 @@
 			following in <filename>dir/.gitattributes</filename>:
 		</para>
 
-		<screen>* filter=git-crypt diff=git-crypt&#10;.gitattributes !filter !diff</screen>
+		<screen>* filter=git-crypt diff=git-crypt merge=git-crypt&#10;.gitattributes !filter !diff !merge</screen>
 
 		<para>
 			The second pattern is essential for ensuring that <filename>.gitattributes</filename> itself
@@ -414,7 +414,7 @@
 			filter in <filename>.gitattributes</filename> as follows:
 		</para>
 
-		<screen><replaceable>secretfile</replaceable> filter=git-crypt-<replaceable>KEYNAME</replaceable> diff=git-crypt-<replaceable>KEYNAME</replaceable></screen>
+		<screen><replaceable>secretfile</replaceable> filter=git-crypt-<replaceable>KEYNAME</replaceable> diff=git-crypt-<replaceable>KEYNAME</replaceable> merge=git-crypt-<replaceable>KEYNAME</replaceable></screen>
 
 		<para>
 			To export an alternative key or share it with a GPG user, pass the