#!/usr/bin/perl -w # Copyright (c) 2007 Anthony Towns # GNU GPL; v2 or later # Gives an overview of what changed between two keyrings # Take from jetring-diff and modified to be suitable for git. # Copyright (c) 2007 Jonathan McDowell use strict; use Cwd q{abs_path}; use File::Temp qw(tempdir); use warnings; use strict; if (@ARGV != 2 and @ARGV != 7) { die "usage: gpg-diff old.gpg new.gpg | path old.gpg old-hex old-mode ". "new.gpg new-hex new-mode\n"; } # avoid gnupg touching ~/.gnupg $ENV{GNUPGHOME}=tempdir("jetring.XXXXXXXXXX", TMPDIR => 1, CLEANUP => 1); my ($l, $r); if (@ARGV == 7) { # Print a diff style header print "gpg-diff a/$ARGV[0] b/$ARGV[4]\n"; print "--- a/$ARGV[0]\n"; print "+++ b/$ARGV[4]\n"; print "\n"; if ($ARGV[4] eq '/dev/null') { print "Key deleted\n"; exit 0; } $l = parse_keyring($ARGV[1]); $r = parse_keyring($ARGV[4]); } else { $l = parse_keyring(shift); $r = parse_keyring(shift); } foreach my $id (sort keys %{$l}) { if (not exists $r->{$id}) { summary("-", @{$l->{$id}}); } else { my $diff=0; my @out; my %rpackets = map { comparable($_->{'details'}) => $_ } @{$r->{$id}}; my %lpackets = map { comparable($_->{'details'}) => 1 } @{$l->{$id}}; foreach my $packet (@{$l->{$id}}) { if (defined($rpackets{comparable($packet->{'details'})})) { push @out, " ".outformat($packet->{'details'}); push @out, comparesigs(\$diff, $packet->{'sigs'}, $rpackets{comparable($packet->{'details'})}->{'sigs'}); } else { push @out, "-".outformat($packet->{'details'}); $diff = 1; } } foreach my $packet (@{$r->{$id}}) { if (! $lpackets{comparable($packet->{'details'})}) { push @out, "+".outformat($packet->{'details'}); $diff = 1; } } print @out if $diff; } } foreach my $id (sort keys %{$r}) { if (not exists $l->{$id}) { summary("+", @{$r->{$id}}); } } sub parse_keyring { my $k=shift; $k=abs_path($k); # annoying gpg.. my $cache=$k.".cache"; my $cached=0; my $kmtime=(stat($k))[9]; if (-e $cache) { my $cmtime=(stat($cache))[9]; if ($kmtime == $cmtime) { open(DUMP, $cache) || die "$cache: $!"; $cached=1; } } if (! $cached) { open(DUMP, "gpg --options /dev/null --no-default-keyring ". "--no-auto-check-trustdb --keyring $k --list-sigs ". "--fixed-list-mode --with-colons -q |") or die "couldn't dump keyring $k: $!"; # Disable caching for the moment # if (! open(CACHE, ">$cache")) { # print STDERR "warning: cannot write cache $cache\n"; $cache=undef; # } } my %keys; my $id; my $packet; while () { if (! $cached && defined $cache) { print CACHE $_; } chomp; my @fields=split(":", $_); $fields[5]="-"; # ignore creation date, varies next if $fields[0] eq 'tru'; if ($fields[0] eq 'pub') { $id=$fields[4]; } if ($fields[0] ne 'sig' && $fields[0] ne 'rev') { if (defined($packet)) { push @{$keys{$id}}, $packet; undef $packet; } $packet->{'details'} = \@fields; } else { if (! defined $id or !defined($packet)) { die "parse error: $_"; next; } push @{$packet->{'sigs'}}, \@fields; } } push @{$keys{$id}}, $packet; close DUMP; if (defined $cache) { close CACHE; utime($kmtime, $kmtime, $cache) || print STDERR "warning: failed setting cache time: $!"; } return \%keys; } sub summary { my $prefix=shift; foreach my $record (@_) { if (ref $record eq 'HASH') { summary($prefix, $record->{$_}) foreach reverse sort keys %$record; } else { if ($record->[0] eq 'pub' || $record->[0] eq 'uid') { print "$prefix".outformat($record); } } } } sub outformat { return join(":", @{shift()})."\n"; } sub comparable { my @record=@{shift()}; if ($record[0] eq 'sig') { # Displayed user ids for sigs vary, so compare different # ones the same. The user-id is what matters. $record[9]=""; } return join(":", @record); } sub comparesigs { my $diff = shift; my $l = shift; my $r = shift; my %lseen = map { comparable($_) => 1 } @{$l}; my %rseen = map { comparable($_) => 1 } @{$r}; my @out; foreach my $record (@{$l}) { if (! $rseen{comparable($record)}) { push @out, "-".outformat($record); ${$diff} = 1; } } foreach my $record (@{$r}) { if (! $lseen{comparable($record)}) { push @out, "+".outformat($record); ${$diff} = 1; } } return @out; }