]> Git — Sourcephile - git-remote-gpg.git/blob - git-remote-gpg
fix $VERSION
[git-remote-gpg.git] / git-remote-gpg
1 #!/usr/bin/perl
2 our $VERSION = '2014.04.29';
3 # License
4 # This file is a git-remote-helpers(1) to use a gpg(1)
5 # as a cryptographic layer below git(1)'s objects.
6 # Copyright (C) 2014 Julien Moutinho
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published
10 # by the Free Software Foundation, either version 3 of the License,
11 # or any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty
15 # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 # See the GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 # Dependencies
21 use strict;
22 use warnings FATAL => qw(all);
23 use Carp;
24 use Cwd;
25 use File::Basename;
26 use File::Copy;
27 use File::Path;
28 use File::Spec::Functions qw(:ALL);
29 use File::Temp;
30 use Getopt::Long;
31 use IPC::Run;
32 # NOTE: to debug: IPCRUNDEBUG=basic|data|details|gory
33 use IO::Handle;
34 use JSON;
35 use POSIX qw(WNOHANG);
36 use URI;
37
38 require Pod::Usage;
39 require Data::Dumper;
40 # Trace utilities
41 sub trace (@) {
42 foreach my $msg (@_) {
43 print STDERR $msg
44 if defined $msg;
45 }
46 }
47 sub debug (@) {
48 my $call = (caller(1))[3];
49 if ($ENV{TRACE}) {
50 trace
51 ( "\e[35mDEBUG\e[m"
52 , "\e[30m\e[1m.", join('.', $call."\e[m")
53 , " ", (map {
54 ref $_ eq 'CODE'
55 ? $_->()
56 : Data::Dumper::Dumper($_)
57 } @_)
58 );
59 }
60 return 1;
61 }
62 sub info (@) {
63 my $call = (caller(1))[3];
64 trace
65 ( "\e[32mINFO\e[m"
66 , "\e[30m\e[1m.", join('.', $call."\e[m")
67 , " ", (ref $_ eq 'CODE'?(join("\n ", $_->()), "\n"):(@_, "\n"))
68 );
69 }
70 sub warning (@) {
71 local $Carp::CarpLevel = 1;
72 carp("\e[33mWARNING\e[m ", @_, "\n\t");
73 }
74 sub error (@) {
75 local $Carp::CarpLevel = 1;
76 croak("\e[31mERROR\e[m ", @_, "\n\t");
77 }
78 # System utilities
79 sub rm (@) {
80 foreach my $file (@_) {
81 debug(sub{"file=$file\n"});
82 if (-e $file) {
83 unlink($file)
84 or error("rm $file");
85 }
86 }
87 }
88 sub mkdir (@) {
89 foreach my $dir (@_) {
90 debug(sub{"dir=$dir\n"});
91 File::Path::make_path($dir, {verbose=>0, error => \my $error});
92 if (@$error) {
93 for my $diag (@$error) {
94 my ($dir, $message) = %$diag;
95 error("dir=$dir: $message");
96 }
97 }
98 }
99 }
100 # grg crypto
101 sub grg_rand ($$) {
102 my ($ctx, $size) = @_;
103 local $_;
104 IPC::Run::run([@{$ctx->{config}->{gpg}}
105 , '--armor', '--gen-rand', '1', $size]
106 , '>', \$_)
107 or error("failed to get random bits");
108 chomp;
109 return $_;
110 }
111 sub grg_hash ($$;$) {
112 my ($ctx, $algo, $run) = @_;
113 $run = sub {return @_} unless defined $run;
114 my $hash;
115 IPC::Run::run($run->([@{$ctx->{config}->{gpg}}
116 , '--with-colons', '--print-md', $algo]
117 , '>', \$hash))
118 or error("failed to hash data");
119 return ((split(':', $hash))[2]);
120 }
121 sub gpg_fingerprint($$$) {
122 my ($ctx, $id, $caps_needed) = @_;
123 my ($output);
124 my %h = ();
125 if (IPC::Run::run([@{$ctx->{config}->{gpg}}
126 , '--fixed-list-mode', '--with-colons', '--with-fingerprint', '--list-keys', $id]
127 , '>', \$output)) {
128 my @lines = split(/\n/,$output);
129 while (my $line = shift @lines) {
130 if (my ($longkeyid, $caps) = $line =~ m/^pub:[^:]*:[^:]*:[^:]*:([^:]*):[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:([^:]+):.*$/) {
131 my $skip = 0;
132 foreach my $cap (@$caps_needed) {
133 if (not ($caps =~ m/$cap/)) {
134 warning("skipping key 0x$longkeyid which has not usable capability: $cap, but matches: `$id'");
135 $skip = 1;
136 }
137 }
138 if (not $skip) {
139 my $fpr = undef;
140 my $uid = undef;
141 while ((not defined $fpr or not defined $uid)
142 and $line = shift @lines) {
143 (not defined $fpr and (($fpr) = $line =~ m/^fpr:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:([0-9A-F]+):.*$/)) or
144 (not defined $uid and (($uid) = $line =~ m/^uid:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:([^:]+):.*$/)) or
145 1;
146 }
147 error("unable to extract fingerprint and user ID")
148 unless defined $fpr
149 and defined $uid;
150 $h{$fpr} = $uid;
151 }
152 }
153 }
154 }
155 error("unable to find any OpenPGP key with usable capability: ".join('', @$caps_needed)." for: `$id'")
156 unless scalar(%h) gt 0;
157 debug(sub{"$id -> "}, \%h);
158 return %h;
159 }
160 sub grg_encrypt_symmetric ($$$;$) {
161 my ($ctx, $clear, $key, $run) = @_;
162 $run = sub {return @_} unless defined $run;
163 IPC::Run::run($run->([@{$ctx->{config}->{gpg}}
164 , '--batch', '--yes'
165 , '--compress-algo', 'none'
166 , '--force-mdc'
167 , '--passphrase-fd', '3'
168 , '--s2k-mode', '1'
169 , '--trust-model', 'always'
170 , '--symmetric']
171 , '<', \$clear, '3<', \$key))
172 or error("failed to encrypt symmetrically data");
173 }
174 sub grg_decrypt_symmetric ($$$;$) {
175 my ($ctx, $key, $run) = @_;
176 $run = sub {return @_} unless defined $run;
177 IPC::Run::run($run->([@{$ctx->{config}->{gpg}}
178 , '--batch', '--no-default-keyring', '--keyring', '/dev/null', '--secret-keyring', '/dev/null'
179 , '--passphrase-fd', '3', '--quiet', '--decrypt']
180 , '3<', \$key))
181 or error("failed to decrypt symmetrically data");
182 }
183 sub grg_encrypt_asymmetric ($$;$) {
184 my ($ctx, $clear, $run) = @_;
185 $run = sub {return @_} unless defined $run;
186 my @recipients =
187 ( (map { ('--recipient', '0x'.$_) } (keys %{$ctx->{config}->{keys}}))
188 , (map { ('--hidden-recipient', '0x'.$_) } (keys %{$ctx->{config}->{'hidden-keys'}})) );
189 @recipients = ('--default-recipient-self')
190 if @recipients == 0;
191 IPC::Run::run($run->([@{$ctx->{config}->{gpg}}
192 , '--batch', '--yes'
193 , '--compress-algo', 'none'
194 , '--trust-model', 'always'
195 , '--sign', '--encrypt'
196 , ($ctx->{config}->{signingkey}->{fpr} ? ('--local-user', $ctx->{config}->{signingkey}->{fpr}) : ())
197 , @recipients ]
198 , '<', \$clear))
199 or error("failed to encrypt asymmetrically data");
200 }
201 sub grg_decrypt_asymmetric ($$;$) {
202 my ($ctx, $run) = @_;
203 my ($clear, $status);
204 $run = sub {return @_} unless defined $run;
205 IPC::Run::run($run->([@{$ctx->{config}->{gpg}}
206 , '--batch', '--no-default-keyring',
207 , '--status-fd', '3', '--quiet', '--decrypt']
208 , '>', \$clear, '3>', \$status))
209 or error("failed to decrypt asymmetrically data");
210 debug(sub{"status=\n$status"});
211 my @lines = split(/\n/,$status);
212 my ($enc_to, $goodsig, $validsig, $validpub, $goodmdc);
213 foreach my $line (@lines) {
214 (not defined $enc_to and (($enc_to) = $line =~ m/^\[GNUPG:\] ENC_TO ([0-9A-F]+).*$/)) or
215 (not defined $goodsig and (($goodsig) = $line =~ m/^\[GNUPG:\] GOODSIG ([0-9A-F]+).*$/)) or
216 (not defined $goodmdc and (($goodmdc) = $line =~ m/^\[GNUPG:\] (GOODMDC)$/)) or
217 (not defined $validsig and not defined $validpub and (($validsig, $validpub)
218 = $line =~ m/^\[GNUPG:\] VALIDSIG ([0-9A-F]+) [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ [^ ]+ ([0-9A-F]+).*$/)) or
219 1;
220 }
221 error("data expected to be encrypted")
222 unless $enc_to;
223 debug(sub{"enc_to=$enc_to\n"});
224 error("data expected to be signed")
225 unless $goodsig;
226 debug(sub{"goodsig=$goodsig\n"});
227 error("modification detection code incorrect")
228 unless $goodmdc;
229 debug(sub{"good_mdc=$goodmdc\n"});
230 error("data signature invalid")
231 unless $validsig and $validpub;
232 debug(sub{"validsig=$validsig\n"});
233 debug(sub{"validpub=$validpub\n"});
234 error("data signature refused")
235 unless exists $ctx->{config}->{keys}->{$validpub}
236 or exists $ctx->{config}->{'hidden-keys'}->{$validpub};
237 debug(sub{"accepted:$validpub\n"});
238 return $clear;
239 }
240 # grg remote I/O
241 sub grg_remote_fetch_file ($) {
242 my ($ctx) = @_;
243 # NOTE: avoid File::Copy::copy().
244 while (my ($file, undef) = each %{$ctx->{remote}->{fetch}}) {
245 my $path = File::Spec->catfile($ctx->{remote}->{uri}->file, $file);
246 if (-r $path) {
247 my $h = $ctx->{remote}->{fetch}->{$file};
248 $h->{path} = $path;
249 $h->{preserve} = 1;
250 }
251 else { return 0; }
252 }
253 return 1;
254 }
255 sub grg_remote_fetch_rsync ($) {
256 my ($ctx) = @_;
257 my $uri = $ctx->{remote}->{uri}->clone;
258 my @src;
259 if ($uri->opaque =~ m{^//}) {
260 $uri->fragment(undef);
261 $uri->query(undef);
262 @src = map { $uri->path($_); $uri->as_string; }
263 (keys %{$ctx->{remote}->{fetch}});
264 }
265 else {
266 my ($authority, $path, $fragment)
267 = $uri->as_string =~ m|^rsync:(?:([^/#:]+):)?([^?#]*)(?:#(.*))?$|;
268 @src = map { "$authority:$path/$_" }
269 (keys %{$ctx->{remote}->{fetch}});
270 }
271 IPC::Run::run([@{$ctx->{config}->{rsync}}
272 , '-i', '--ignore-times', '--inplace', '--progress'
273 , @src
274 , $ctx->{'dir-cache'}.'/']
275 , '>&2')
276 }
277 sub grg_remote_fetch_sftp ($) {
278 my ($ctx) = @_;
279 IPC::Run::run([@{$ctx->{config}->{curl}}
280 , '--show-error'
281 , '--output', File::Spec->catfile($ctx->{'dir-cache'}, '#1')
282 , File::Spec->catfile($ctx->{remote}->{uri}->as_string
283 , '{'.join(',', (keys %{$ctx->{remote}->{fetch}})).'}') ])
284 }
285 sub grg_remote_fetch ($$) {
286 my ($ctx, $files) = @_;
287 debug(sub{'files='}, $files);
288 my $scheme = $ctx->{remote}->{uri}->scheme;
289 $ctx->{remote}->{fetch}
290 = {map { $_ =>
291 { path => File::Spec->catfile($ctx->{'dir-cache'}, $_)
292 , preserve => 0 }
293 } @$files};
294 my $fct =
295 { file => \&grg_remote_fetch_file
296 , rsync => \&grg_remote_fetch_rsync
297 , sftp => \&grg_remote_fetch_sftp
298 }->{$scheme};
299 error("URL scheme not supported: `$scheme'")
300 unless $fct;
301 $fct->($ctx)
302 or $ctx->{remote}->{fetch} = {};
303 return $ctx->{remote}->{fetch};
304 }
305 sub grg_remote_init_file ($) {
306 my ($ctx) = @_;
307 my $dst = $ctx->{remote}->{uri}->file;
308 &mkdir($dst);
309 return 1;
310 }
311 sub grg_remote_init_rsync ($) {
312 my ($ctx) = @_;
313 my $tmp = File::Temp->tempdir('grg_rsync_XXXXXXXX', CLEANUP => 1);
314 my $uri = $ctx->{remote}->{uri}->clone;
315 my ($path, $dst);
316 if ($uri->opaque =~ m{^//}) {
317 $uri->fragment(undef);
318 $uri->query(undef);
319 $path = $uri->path;
320 $dst = $uri->as_string;
321 }
322 else {
323 my ($authority, $fragment);
324 ($authority, $path, $fragment)
325 = $uri->as_string =~ m|^rsync:(?:([^/#:]+):)?([^?#]*)(?:#(.*))?$|;
326 $dst = "$authority:";
327 }
328 &mkdir(File::Spec->catdir($tmp, $path));
329 IPC::Run::run([@{$ctx->{config}->{rsync}}
330 , '-i', '--recursive', '--relative'
331 , '--exclude=*', '.'
332 , $dst]
333 , '>&2'
334 , init => sub { chdir $tmp or die $!; })
335 }
336 sub grg_remote_init_sftp ($) {
337 my ($ctx) = @_;
338 my $path = $ctx->{remote}->{uri}->path;
339 my $uri = $ctx->{remote}->{uri}->clone;
340 $uri->fragment(undef);
341 $uri->path(undef);
342 $uri->query(undef);
343 IPC::Run::run([@{$ctx->{config}->{curl}}
344 , '--show-error', '--ftp-create-dirs'
345 , '-Q', "+mkdir ".$path
346 , $uri->as_string])
347 }
348 sub grg_remote_init ($) {
349 my ($ctx) = @_;
350 my $scheme = $ctx->{remote}->{uri}->scheme;
351 my $fct =
352 { file => \&grg_remote_init_file
353 , rsync => \&grg_remote_init_rsync
354 , sftp => \&grg_remote_init_sftp
355 }->{$scheme};
356 error("URL scheme not supported: `$scheme'")
357 unless $fct;
358 $fct->($ctx)
359 or error("remote init failed");
360 return;
361 }
362 sub grg_remote_push_file ($) {
363 my ($ctx) = @_;
364 my $ok = 1;
365 foreach my $file (@{$ctx->{remote}->{push}}) {
366 my $src = File::Spec->catfile($ctx->{'dir-cache'}, $file);
367 my $dst = File::Spec->catfile($ctx->{remote}->{uri}->file, $file);
368 debug(sub{"File::Copy::move('$src', '$dst')\n"});
369 if (not File::Copy::move($src, $dst)) {
370 $ok = 0;
371 last;
372 }
373 }
374 return $ok;
375 }
376 sub grg_remote_push_rsync ($) {
377 my ($ctx) = @_;
378 my $uri = $ctx->{remote}->{uri}->clone;
379 $uri->fragment(undef);
380 $uri->query(undef);
381 my ($path, $dst);
382 if ($uri->opaque =~ m{^//}) {
383 $uri->fragment(undef);
384 $uri->query(undef);
385 $dst = $uri->as_string;
386 }
387 else {
388 my ($authority, $path, $fragment)
389 = $uri->as_string =~ m|^rsync:(?:([^/#:]+):)?([^?#]*)(?:#(.*))?$|;
390 $dst = "$authority:$path/";
391 }
392 IPC::Run::run([@{$ctx->{config}->{rsync}}
393 , '-i', '--relative'
394 , (@{$ctx->{remote}->{push}})
395 , $dst]
396 , '>&2'
397 , init => sub { chdir $ctx->{'dir-cache'} or die $!; });
398 }
399 sub grg_remote_push_sftp ($) {
400 my ($ctx) = @_;
401 my $uri = $ctx->{remote}->{uri}->clone;
402 $uri->fragment('');
403 $uri->query('');
404 IPC::Run::run([@{$ctx->{config}->{curl}}
405 , '--show-error', '--ftp-create-dirs', '--upload-file'
406 , '{'.join(',', @{$ctx->{remote}->{push}}).'}'
407 , $uri->as_string.'/'])
408 }
409 sub grg_remote_push ($) {
410 my ($ctx) = @_;
411 my $scheme = $ctx->{remote}->{uri}->scheme;
412 grg_remote_init($ctx)
413 unless $ctx->{remote}->{checked};
414 return 1
415 if @{$ctx->{remote}->{push}} == 0;
416 my $fct =
417 { file => \&grg_remote_push_file
418 , rsync => \&grg_remote_push_rsync
419 , sftp => \&grg_remote_push_sftp
420 }->{$scheme};
421 error("URL scheme not supported: `$scheme'")
422 unless $fct;
423 $fct->($ctx)
424 or error("remote push failed");
425 rm(map {File::Spec->catfile($ctx->{'dir-cache'}, $_)} @{$ctx->{remote}->{push}});
426 return 1;
427 }
428 sub grg_remote_remove ($) {
429 my ($ctx) = @_;
430 #my $scheme = $ctx->{remote}->{uri}->scheme;
431 #my $fct =
432 # { file => sub {
433 # File::Copy::remove_tree
434 # ( map { File::Spec->catfile($ctx->{remote}->{uri}->path, $_) } @$files
435 # , verbose => 1 )
436 # }
437 # , rsync => sub {
438 # IPC::Run::run([@{$ctx->{config}->{rsync}}
439 # , '--verbose', '--ignore-times', '--recursive', '--delete'
440 # , @$files
441 # , $ctx->{remote}->{uri}])
442 # }
443 # , sftp => sub {
444 # IPC::Run::run([@{$ctx->{config}->{curl}}
445 # , '--show-error'
446 # , map { ('-Q', 'rm '.$_) } @$files
447 # , $ctx->{remote}->{uri}])
448 # }
449 # }->{$scheme};
450 #error("URL scheme not supported: `$scheme'")
451 # unless $fct;
452 #$fct->($ctx, $ctx->{remote}->{remove})
453 # or error("remote remove failed");
454 #return;
455 }
456 # grg packing
457 sub grg_pack_fetch ($$) {
458 my ($ctx, $fetch_objects) = @_;
459 local $_;
460 # %remote_objects
461 my %remote_objects = ();
462 while (my ($pack_id, $pack) = each %{$ctx->{manifest}->{packs}}) {
463 foreach my $obj (@{$pack->{objects}}) {
464 $remote_objects{$obj} = $pack_id;
465 }
466 }
467 # @packs_to_fetch
468 my %packs_to_fetch = ();
469 foreach my $obj (@$fetch_objects) {
470 my @packs = ($remote_objects{$obj});
471 while (my $pack_id = shift @packs) {
472 if (not exists $packs_to_fetch{$pack_id}) {
473 $packs_to_fetch{$pack_id} = 1;
474 my $manifest_pack = $ctx->{manifest}->{packs}->{$pack_id};
475 error("manifest is missing a dependency pack: $pack_id")
476 unless defined $manifest_pack;
477 @packs = (@packs, @{$manifest_pack->{deps}});
478 }
479 }
480 }
481 my @packs_to_fetch = keys %packs_to_fetch;
482 my $packs_fetched = grg_remote_fetch($ctx, [@packs_to_fetch]);
483 foreach my $pack_id (@packs_to_fetch) {
484 my $pack_fetched
485 = exists $packs_fetched->{$pack_id}
486 ? $packs_fetched->{$pack_id}
487 : {path => File::Spec->catfile($ctx->{'dir-cache'}, $pack_id), preserve => 0};
488 my $manifest_pack = $ctx->{manifest}->{packs}->{$pack_id};
489 my $pack_key = $manifest_pack->{key};
490 my $pack_data;
491 grg_decrypt_symmetric($ctx, $pack_key, sub {
492 push @{$_[0]}, ($pack_fetched->{path});
493 return (@_, '>', \$pack_data);
494 });
495 my $pack_hash_algo = $manifest_pack->{hash_algo};
496 my $pack_hash = grg_hash($ctx
497 , $pack_hash_algo
498 , sub { return (@_, '<', \$pack_data); });
499 error("pack data hash differs from pack manifest hash")
500 unless $pack_hash eq $manifest_pack->{hash};
501 rm($pack_fetched)
502 unless $pack_fetched->{preserve};
503 IPC::Run::run(['git', 'index-pack', '-v', '--stdin']
504 , '<', \$pack_data
505 , '>&2');
506 }
507 }
508 sub grg_pack_push ($$) {
509 my ($ctx, $push_objects) = @_;
510 local $_;
511 debug(sub{"push_objects=\n"}, $push_objects);
512 # %remote_objects
513 my %remote_objects = ();
514 while (my ($pack_id, $pack) = each %{$ctx->{manifest}->{packs}}) {
515 foreach my $obj (@{$pack->{objects}}) {
516 $remote_objects{$obj} = $pack_id;
517 }
518 }
519 # @common_objects
520 IPC::Run::run(['git', 'cat-file', '--batch-check']
521 , '<', \join("\n", keys %remote_objects)
522 , '>', \$_)
523 or error("failed to query local git objects");
524 my @common_objects
525 = map {
526 if ($_ =~ m/ missing$/) { () }
527 else { s/ .*//; $_ }
528 } (split(/\n/, $_));
529 # @pack_objects, @pack_deps_objects
530 IPC::Run::run(['git', 'rev-list', '--objects-edge', '--stdin', '--']
531 , '<', \join("\n", ((map {'^'.$_} @common_objects), @$push_objects))
532 , '>', \$_)
533 or error("failed to query objects to pack");
534 my @pack_objects_edge = split(/\n/, $_);
535 foreach (@pack_objects_edge) {s/ .*//}
536 my @pack_objects = grep {m/^[^-]/} @pack_objects_edge;
537 my @pack_deps_objects = grep {s/^-//} @pack_objects_edge;
538 # %pack_deps
539 my %pack_deps = ();
540 foreach my $obj (@pack_deps_objects) {
541 my $pack = $remote_objects{$obj};
542 error("manifest is missing object dependencies")
543 unless defined $pack;
544 $pack_deps{$pack} = 1;
545 }
546 if (@pack_objects > 0) {
547 # $pack_id
548 my $pack_id;
549 my $pack_id_try = 0;
550 while (not defined $pack_id
551 or exists $ctx->{manifest}->{packs}->{$pack_id}) {
552 $pack_id = grg_rand($ctx, $ctx->{config}->{'pack-filename-size'});
553 $pack_id =~ s{/}{-}g;
554 error("failed to pick an unused random pack filename after 512 tries; retry or increase grg.pack-filename-size")
555 if $pack_id_try++ >= 512;
556 }
557 my $pack_key = grg_rand($ctx, $ctx->{config}->{'pack-key-size'});
558 my $pack_data;
559 IPC::Run::run(['git', 'pack-objects', '--stdout']
560 , '<', \join("\n", @pack_objects)
561 , '>', \$pack_data)
562 or error("failed to pack objects to push");
563 my $pack_hash = grg_hash($ctx
564 , $ctx->{config}->{'pack-hash-algo'}
565 , sub { return (@_, '<', \$pack_data); });
566 grg_encrypt_symmetric($ctx, $pack_data, $pack_key, sub {
567 push @{$_[0]}, ('--output', File::Spec->catfile($ctx->{'dir-cache'}, $pack_id));
568 return @_;
569 });
570 push @{$ctx->{remote}->{push}}, $pack_id;
571 $ctx->{manifest}->{packs}->{$pack_id} =
572 { deps => [keys %pack_deps]
573 , hash => $pack_hash
574 , hash_algo => $ctx->{config}->{'pack-hash-algo'}
575 , key => $pack_key
576 , objects => \@pack_objects
577 };
578 }
579 }
580 # grg manifest
581 sub grg_manifest_fetch ($) {
582 my ($ctx) = @_;
583 $ctx->{manifest} =
584 { 'hidden-keys' => {}
585 , keys => {}
586 , packs => {}
587 , refs => {}
588 , version => undef
589 };
590 my $fetched = grg_remote_fetch($ctx, [$ctx->{'manifest-file'}]);
591 my $crypt = $fetched->{$ctx->{'manifest-file'}}->{path};
592 if (defined $crypt) {
593 $ctx->{remote}->{checked} = 1;
594 my $json;
595 grg_decrypt_asymmetric($ctx, sub {
596 push @{$_[0]}, $crypt;
597 return (@_, '>', \$json); });
598 rm($fetched->{$ctx->{'manifest-file'}}->{path})
599 unless $fetched->{$ctx->{'manifest-file'}}->{preserve};
600 my $manifest;
601 ($manifest = JSON::decode_json($json) and ref $manifest eq 'HASH')
602 or error("failed to decode JSON manifest");
603 $ctx->{manifest} = {%{$ctx->{manifest}}, %$manifest};
604 foreach my $slot (qw(keys hidden-keys)) {
605 while (my ($fpr, $uid) = each %{$ctx->{manifest}->{$slot}}) {
606 my %keys = gpg_fingerprint($ctx, '0x'.$fpr, ['E']);
607 my ($fpr, $uid) = each %keys;
608 $ctx->{config}->{$slot}->{$fpr} = $uid;
609 }
610 }
611 }
612 else {
613 if ($ctx->{command} eq 'push' or $ctx->{command} eq 'list for-push') {
614 $ctx->{remote}->{checked} = 0;
615 }
616 elsif ($ctx->{remote}->{checking}) {
617 exit 100;
618 }
619 else {
620 error("remote checking failed");
621 }
622 }
623 }
624 sub grg_manifest_push ($) {
625 my ($ctx) = @_;
626 foreach my $slot (qw(keys hidden-keys)) {
627 $ctx->{manifest}->{$slot} = {};
628 while (my ($fpr, $uid) = each %{$ctx->{config}->{$slot}}) {
629 $ctx->{manifest}->{$slot}->{$fpr} = $uid;
630 }
631 }
632 my $json = JSON::encode_json($ctx->{manifest})
633 or error("failed to encode JSON manifest");
634 grg_encrypt_asymmetric($ctx, $json, sub {
635 push @{$_[0]}
636 , ('--output', File::Spec->catfile($ctx->{'dir-cache'}, $ctx->{'manifest-file'}));
637 return @_; });
638 push @{$ctx->{remote}->{push}}, $ctx->{'manifest-file'};
639 }
640 # grg config
641 sub grg_config_read($) {
642 my ($ctx) = @_;
643 my $cfg = $ctx->{config};
644 local $/ = "\n";
645
646 foreach my $name (qw(gpg signingkey keys)
647 , grep { !m/^(gpg|signingkey|keys)$/ } (keys %$cfg)) {
648 my $value;
649 IPC::Run::run(['git', 'config', '--get', 'remote.'.$ctx->{remote}->{name}.'.'.$name, '.+'], '>', \$value) or
650 IPC::Run::run(['git', 'config', '--get', 'grg.'.$name, '.+'], '>', \$value) or 1;
651 if ($name eq 'signingkey') {
652 IPC::Run::run(['git', 'config', '--get', 'user.'.$name, '.+'], '>', \$value)
653 if (not $value);
654 chomp $value;
655 my %keys = gpg_fingerprint($ctx, $value, ['S']);
656 warning("signing key ID is not matching a unique key: taking only one")
657 unless scalar(keys %keys) == 1;
658 my ($fpr, $uid) = each %keys;
659 $cfg->{$name} = {fpr => $fpr, uid => $uid};
660 }
661 elsif ($name eq 'keys' or $name eq 'hidden-keys') {
662 IPC::Run::run(['git', 'config', '--get', 'user.'.$name, '.+'], '>', \$value)
663 if (not $value);
664 chomp $value;
665 my @ids = split(/,/, $value);
666 if (@ids > 0) {
667 foreach my $key (@ids) {
668 my %keys = gpg_fingerprint($ctx, $key, ['E']);
669 while (my ($fpr, $uid) = each %keys) {
670 $cfg->{$name}->{$fpr} = $uid;
671 }
672 }
673 }
674 }
675 elsif (grep(/^$name$/, qw(curl gpg rsync))) {
676 IPC::Run::run(['git', 'config', '--get', $name.'.program', '.+'], '>', \$value)
677 if (not $value);
678 $cfg->{$name} = [split(' ', $value)]
679 if $value;
680 }
681 else {
682 chomp $value;
683 $cfg->{$name} = $value
684 if $value;
685 }
686 }
687 error("no signingkey configured; to do so you may use one of following commands:\n"
688 , "\t\$ git config remote.'$ctx->{remote}->{name}'.signingkey \$your_openpgp_id\n"
689 , "\t\$ git config grg.signingkey \$your_openpgp_id\n"
690 , "\t\$ git config user.signingkey \$your_openpgp_id"
691 ) unless defined $cfg->{signingkey};
692 if ( (scalar (keys %{$cfg->{keys}}) == 0)
693 and (scalar (keys %{$cfg->{'hidden-keys'}}) == 0) ) {
694 $cfg->{keys} = { $cfg->{signingkey}->{fpr} => $cfg->{signingkey}->{uid} };
695 }
696
697 debug(sub{'config='},$cfg);
698 }
699 # grg system
700 sub grg_connect ($) {
701 my ($ctx) = @_;
702 grg_config_read($ctx);
703 grg_manifest_fetch($ctx);
704 }
705 sub grg_disconnect ($) {
706 my ($ctx) = @_;
707 grg_remote_push($ctx);
708 }
709 # grg commands
710 sub gpg_command_answer ($) {
711 my @cmd = @_;
712 debug(sub{join('', @cmd)."\n"});
713 print STDOUT (@cmd, "\n");
714 }
715 sub grg_command_capabilities ($) {
716 my ($ctx) = @_;
717 $ctx->{command} = 'capabilities';
718 gpg_command_answer("fetch");
719 gpg_command_answer("push");
720 gpg_command_answer("");
721 STDOUT->flush;
722 }
723 sub grg_command_fetch ($$) {
724 my ($ctx, $fetch_refs) = @_;
725 $ctx->{command} = 'fetch';
726 debug(sub{"fetch_refs="}, $fetch_refs);
727 grg_connect($ctx);
728 # @fetch_objects
729 my @fetch_objects= ();
730 foreach my $ref (@$fetch_refs) {
731 push @fetch_objects, $ref->{sha1};
732 }
733 grg_pack_fetch($ctx, \@fetch_objects);
734 }
735 sub grg_command_list ($$) {
736 my ($ctx, $command) = @_;
737 $ctx->{command} = $command;
738 grg_connect($ctx);
739 while (my ($ref, $obj) = each %{$ctx->{manifest}->{refs}}) {
740 gpg_command_answer("$obj $ref");
741 };
742 gpg_command_answer("");
743 }
744 sub grg_command_push ($$) {
745 my ($ctx, $push_refs) = @_;
746 local $_;
747 $ctx->{command} = 'push';
748 debug(sub{"push_refs="}, $push_refs);
749 grg_connect($ctx);
750 # @push_objects
751 my @push_objects= ();
752 foreach my $ref (@$push_refs) {
753 IPC::Run::run(['git', 'rev-list', '--ignore-missing', '--max-count=1', $ref->{src}, '--']
754 , '>', \$_)
755 or error("failed to dereference ref to push: ".$ref->{src});
756 chomp;
757 $ref->{src_obj} = $_;
758 push @push_objects, $_;
759 }
760 grg_pack_push($ctx, \@push_objects);
761 my $manifest_refs = $ctx->{manifest}->{refs};
762 foreach my $ref (@$push_refs) {
763 $manifest_refs->{$ref->{dst}} = $ref->{src_obj};
764 }
765 $manifest_refs->{HEAD}
766 = $push_refs->[-1]->{src_obj}
767 unless @$push_refs == 0;
768 grg_manifest_push($ctx);
769 grg_disconnect($ctx);
770 }
771 sub grg_commands(@) {
772 my ($ctx) = @_;
773 my $line = undef;
774 local $/ = "\n";
775 #STDOUT->autoflush(1);
776 while (defined $line or (not eof(*STDIN) and
777 (defined($line = readline(*STDIN)))
778 ? (chomp $line or 1)
779 : error("readline failed: $!")
780 )) {
781 debug(sub{"line=\"",$line,"\"\n"});
782 $ctx->{command} = undef;
783 if ($line eq 'capabilities') {
784 grg_command_capabilities($ctx);
785 $line = undef;
786 }
787 elsif ($line =~ m/^fetch .*$/) {
788 my @refs = ();
789 my ($sha1, $name);
790 while ((defined $line or (not eof(*STDIN) and
791 ((defined($line = readline(*STDIN)))
792 ? (chomp $line or 1)
793 : error("readline failed: $!")))) and
794 (($sha1, $name) = ($line =~ m/^fetch ([0-9a-f]{40}) (.+)$/))
795 ) {
796 debug(sub{"fetch line=\"",$line,"\"\n"});
797 push @refs, {sha1=>$sha1, name=>$name};
798 $line = undef;
799 }
800 error("failed to parse command: $line")
801 if @refs == 0;
802 grg_command_fetch($ctx, \@refs);
803 }
804 elsif ($line eq 'list' or $line eq 'list for-push') {
805 grg_command_list($ctx, $line);
806 $line = undef;
807 }
808 elsif ($line =~ m/^push .*$/) {
809 my @refs = ();
810 my ($force, $src, $dst);
811 while ((defined $line or (not eof(*STDIN) and
812 ((defined($line = readline(*STDIN)))
813 ? (chomp $line or 1)
814 : error("readline failed: $!")))) and
815 (($force, $src, $dst) = ($line =~ m/^push (\+)?([^:]+):(.+)$/))
816 ) {
817 debug(sub{"push line=\"",$line,"\"\n"});
818 push @refs, {force=>(defined $force), src=>$src, dst=>$dst};
819 $line = undef;
820 }
821 error("failed to parse command: $line")
822 if @refs == 0;
823 grg_command_push($ctx, \@refs);
824 }
825 elsif ($line =~ m/^$/) {
826 $line = undef;
827 {
828 local $SIG{'PIPE'} = 'IGNORE';
829 gpg_command_answer("");
830 }
831 return 0;
832 }
833 else {
834 warning("unsupported command supplied: `$line'");
835 $line = undef;
836 }
837 }
838 }
839 sub main {
840 $ENV{GIT_DIR} = $ENV{GIT_DIR} || '.git';
841 $ENV{GITCEPTION} = ($ENV{GITCEPTION} || '') . '+';
842 my $ctx =
843 { command => undef
844 , config =>
845 { curl => ['curl']
846 , gpg => ['gpg']
847 , keys => {}
848 , 'hidden-keys' => {}
849 , 'manifest-hash-algo' => 'SHA224' # NOTE: SHA512, SHA384, SHA256, SHA224 supported.
850 , 'pack-filename-size' => 42
851 , 'pack-hash-algo' => 'SHA224' # NOTE: SHA512, SHA384, SHA256, SHA224 supported.
852 , 'pack-key-size' => 64
853 , signingkey => undef
854 , rsync => ['rsync']
855 }
856 , 'dir-cache' => undef
857 , manifest => {}
858 , 'manifest-file' => undef
859 , remote =>
860 { checking => 0
861 , checked => undef
862 , name => undef
863 , uri => undef
864 , push => []
865 }
866 };
867 Getopt::Long::Configure
868 ( 'auto_version'
869 , 'pass_through'
870 , 'require_order'
871 );
872 Getopt::Long::GetOptions
873 ( help => sub { Pod::Usage::pod2usage
874 ( -exitstatus => 0
875 , -sections => ['SYNOPSIS', 'OPTIONS', 'REMOTES', 'CONFIG']
876 , -verbose => 99 ); }
877 , man => sub { Pod::Usage::pod2usage(-verbose => 2); }
878 , check => sub {
879 $ctx->{remote}->{checking} = 1;
880 }
881 );
882 if (not $ctx->{remote}->{checking}) {
883 my $name = shift @ARGV;
884 Pod::Usage::pod2usage(-verbose => 1)
885 unless defined $name;
886 ($ctx->{remote}->{name}) = ($name =~ m/^((\w|-)+)$/);
887 error("valid name of remote Git required, got: `$name'")
888 unless $ctx->{remote}->{name};
889 }
890 my $uri = shift @ARGV;
891 Pod::Usage::pod2usage(-verbose => 1)
892 unless defined $uri;
893 $ctx->{remote}->{uri} = URI->new($uri);
894 error("valid URL of remote Git required, got: `$uri'")
895 unless $ctx->{remote}->{uri};
896 my $fragment = $ctx->{remote}->{uri}->fragment;
897 $fragment = ''
898 unless defined $fragment;
899 $ctx->{'manifest-file'} = grg_hash($ctx
900 , $ctx->{config}->{'manifest-hash-algo'}
901 , sub { return (@_, '<', \$fragment); });
902 if (-d $ENV{GIT_DIR}) {
903 $ctx->{'dir-cache'} = File::Spec->catdir
904 ( $ENV{GIT_DIR}, 'cache', 'remotes'
905 , $ctx->{remote}->{name}, 'gpg');
906 &mkdir($ctx->{'dir-cache'});
907 }
908 else {
909 $ctx->{'dir-cache'} = File::Temp->tempdir('grg_cache_XXXXXXXX', CLEANUP => 1);
910 }
911 debug(sub{"ctx="},$ctx);
912 grg_commands($ctx);
913 }
914 main;
915 1;
916 __END__
917
918 =encoding utf8
919
920 =head1 NAME
921
922 git-remote-gpg - git-remote-helpers(1) to encrypt remote repository through gpg(1)
923
924 =head1 SYNOPSIS
925
926 =item git-remote-gpg $gpg_remote $gpg_url
927
928 =item git-remote-gpg --check $gpg_url
929
930 =head1 OPTIONS
931
932 =over 8
933
934 =item B<-h>, B<--help>
935
936 =item B<--version>
937
938 =back
939
940 =head1 REMOTES
941
942 =head2 Via rsync(1)
943
944 =item git remote add $remote gpg::rsync:${user:+$user@}$host:$path
945
946 =item git remote add $remote gpg::rsync://${user:+$user@}$host${port:+:$port}/$path
947
948 =head2 Via curl(1)
949
950 =item git remote add $remote gpg::sftp://${user:+$user@}$host${port:+:$port}/$path
951
952 =head2 Via File::Copy(3pm)
953
954 =item git remote add $remote gpg::file://$path
955
956 =head1 CONFIG
957
958 =head2 git-config(1)
959
960 =over 8
961
962 =item B<grg.curl>
963
964 =item B<grg.gpg>
965
966 =item B<grg.keys>
967
968 =item B<grg.hidden-keys>
969
970 =item B<grg.manifest-hash-algo>
971
972 =item B<grg.pack-filename-size>
973
974 =item B<grg.pack-hash-algo>
975
976 =item B<grg.pack-key-size>
977
978 =item B<grg.signingkey>
979
980 =item B<grg.rsync>
981
982 =back
983
984 =cut