1#!perl
2
3	## sha3sum: filter for computing SHA-3 digests (ref. shasum)
4	##
5	## Copyright (C) 2012-2018 Mark Shelor, All Rights Reserved
6	##
7	## Version: 1.04
8	## Fri Apr 20 16:25:30 MST 2018
9
10	## sha3sum SYNOPSIS adapted from GNU Coreutils sha1sum. Add
11	## "-a" option for algorithm selection,
12	## "-U" option for Universal Newlines support, and
13	## "-0" option for reading bit strings.
14
15BEGIN { pop @INC if $INC[-1] eq '.' }
16
17use strict;
18use warnings;
19use Fcntl;
20use Getopt::Long;
21use Digest::SHA3 qw($errmsg);
22
23my $POD = <<'END_OF_POD';
24
25=head1 NAME
26
27sha3sum - Print or Check SHA-3 Checksums
28
29=head1 SYNOPSIS
30
31 Usage: sha3sum [OPTION]... [FILE]...
32 Print or check SHA-3 checksums.
33 With no FILE, or when FILE is -, read standard input.
34
35   -a, --algorithm   224 (default), 256, 384, 512, 128000, 256000
36   -b, --binary      read in binary mode
37   -c, --check       read SHA-3 sums from the FILEs and check them
38       --tag         create a BSD-style checksum
39   -t, --text        read in text mode (default)
40   -U, --UNIVERSAL   read in Universal Newlines mode
41                         produces same digest on Windows/Unix/Mac
42   -0, --01          read in BITS mode
43                         ASCII '0' interpreted as 0-bit,
44                         ASCII '1' interpreted as 1-bit,
45                         all other characters ignored
46
47 The following five options are useful only when verifying checksums:
48       --ignore-missing  don't fail or report status for missing files
49   -q, --quiet           don't print OK for each successfully verified file
50   -s, --status          don't output anything, status code shows success
51       --strict          exit non-zero for improperly formatted checksum lines
52   -w, --warn            warn about improperly formatted checksum lines
53
54   -h, --help        display this help and exit
55   -v, --version     output version information and exit
56
57 The sums are computed as described in the FIPS 202 SHA-3 submission.
58 When checking, the input should be a former output of this program.
59 The default mode is to print a line with checksum, a character
60 indicating type (`*' for binary, ` ' for text, `U' for UNIVERSAL,
61 `^' for BITS), and name for each FILE.  The line starts with a `\'
62 character if the FILE name contains either newlines or backslashes,
63 which are then replaced by the two-character sequences `\n' and
64 `\\' respectively.
65
66 Report sha3sum bugs to mshelor@cpan.org
67
68=head1 DESCRIPTION
69
70Running I<sha3sum> is often the quickest way to compute SHA-3 message
71digests.  The user simply feeds data to the script through files or
72standard input, and then collects the results from standard output.
73
74The following command shows how to compute digests for typical inputs
75such as the NIST test vector "abc":
76
77	perl -e "print qq(abc)" | sha3sum
78
79Or, if you want to use SHA3-256 instead of the default SHA3-224,
80simply say:
81
82	perl -e "print qq(abc)" | sha3sum -a 256
83
84Unlike many other digest computation programs, I<sha3sum> implements
85the full SHA-3 standard by allowing partial-byte inputs, which can
86be recognized through the BITS option (I<-0>).  The following example
87computes the SHA3-384 digest of the 7-bit message I<0001100>:
88
89	perl -e "print qq(0001100)" | sha3sum -0 -a 384
90
91=head1 AUTHOR
92
93Copyright (C) 2012-2018 Mark Shelor <mshelor@cpan.org>.
94
95=head1 SEE ALSO
96
97I<sha3sum> is implemented using the Perl module L<Digest::SHA3>.
98
99=cut
100
101END_OF_POD
102
103my $VERSION = "1.04";
104
105sub usage {
106	my($err, $msg) = @_;
107
108	$msg = "" unless defined $msg;
109	if ($err) {
110		warn($msg . "Type sha3sum -h for help\n");
111		exit($err);
112	}
113	my($USAGE) = $POD =~ /SYNOPSIS(.+?)^=/sm;
114	$USAGE =~ s/^\s*//;
115	$USAGE =~ s/\s*$//;
116	$USAGE =~ s/^ //gm;
117	print $USAGE, "\n";
118	exit($err);
119}
120
121
122	## Sync stdout and stderr by forcing a flush after every write
123
124select((select(STDOUT), $| = 1)[0]);
125select((select(STDERR), $| = 1)[0]);
126
127
128	## Collect options from command line
129
130my ($alg, $binary, $check, $text, $status, $quiet, $warn, $help);
131my ($version, $BITS, $UNIVERSAL, $tag, $strict, $ignore_missing);
132
133eval { Getopt::Long::Configure ("bundling") };
134GetOptions(
135	'b|binary' => \$binary, 'c|check' => \$check,
136	't|text' => \$text, 'a|algorithm=i' => \$alg,
137	's|status' => \$status, 'w|warn' => \$warn,
138	'q|quiet' => \$quiet,
139	'h|help' => \$help, 'v|version' => \$version,
140	'U|UNIVERSAL' => \$UNIVERSAL,
141	'0|01' => \$BITS,
142	'tag' => \$tag,
143	'strict' => \$strict,
144	'ignore-missing' => \$ignore_missing,
145) or usage(1, "");
146
147
148	## Deal with help requests and incorrect uses
149
150usage(0)
151	if $help;
152usage(1, "sha3sum: Ambiguous file mode\n")
153	if scalar(grep {defined $_}
154		($binary, $text, $BITS, $UNIVERSAL)) > 1;
155usage(1, "sha3sum: --warn option used only when verifying checksums\n")
156	if $warn && !$check;
157usage(1, "sha3sum: --status option used only when verifying checksums\n")
158	if $status && !$check;
159usage(1, "sha3sum: --quiet option used only when verifying checksums\n")
160	if $quiet && !$check;
161usage(1, "sha3sum: --ignore-missing option used only when verifying checksums\n")
162	if $ignore_missing && !$check;
163usage(1, "sha3sum: --strict option used only when verifying checksums\n")
164	if $strict && !$check;
165usage(1, "sha3sum: --tag does not support --text mode\n")
166	if $tag && $text;
167usage(1, "sha3sum: --tag does not support Universal Newlines mode\n")
168	if $tag && $UNIVERSAL;
169usage(1, "sha3sum: --tag does not support BITS mode\n")
170	if $tag && $BITS;
171
172
173	## Default to SHA3-224 unless overridden by command line option
174
175my %isAlg = map { $_ => 1 } (224, 256, 384, 512, 128000, 256000);
176$alg = 224 unless defined $alg;
177usage(1, "sha3sum: Unrecognized algorithm\n") unless $isAlg{$alg};
178
179my %Tag = map { $_ => "SHA3-$_" } (224, 256, 384, 512);
180$Tag{128000} = "SHAKE128";
181$Tag{256000} = "SHAKE256";
182
183
184	## Display version information if requested
185
186if ($version) {
187	print "$VERSION\n";
188	exit(0);
189}
190
191
192	## Try to figure out if the OS is DOS-like.  If it is,
193	## default to binary mode when reading files, unless
194	## explicitly overridden by command line "--text" or
195	## "--UNIVERSAL" options.
196
197my $isDOSish = ($^O =~ /^(MSWin\d\d|os2|dos|mint|cygwin)$/);
198if ($isDOSish) { $binary = 1 unless $text || $UNIVERSAL }
199
200my $modesym = $binary ? '*' : ($UNIVERSAL ? 'U' : ($BITS ? '^' : ' '));
201
202
203	## Read from STDIN (-) if no files listed on command line
204
205@ARGV = ("-") unless @ARGV;
206
207
208	## sumfile($file): computes SHA-3 digest of $file
209
210sub sumfile {
211	my $file = shift;
212
213	my $mode = $binary ? 'b' : ($UNIVERSAL ? 'U' : ($BITS ? '0' : ''));
214	my $digest = eval { Digest::SHA3->new($alg)->addfile($file, $mode) };
215	if ($@) { warn "sha3sum: $file: $errmsg\n"; return }
216	$digest->hexdigest;
217}
218
219
220	## %len2alg: maps hex digest length to SHA-3 algorithm
221
222my %len2alg = (56 => 224, 64 => 256, 96 => 384, 128 => 512,
223		336 => 128000, 272 => 256000);
224
225
226	## unescape: convert backslashed filename to plain filename
227
228sub unescape {
229	$_ = shift;
230	s/\\\\/\0/g;
231	s/\\n/\n/g;
232	s/\0/\\/g;
233	return $_;
234}
235
236
237	## verify: confirm the digest values in a checksum file
238
239sub verify {
240	my $checkfile = shift;
241	my ($err, $fmt_errs, $read_errs, $match_errs) = (0, 0, 0, 0);
242	my ($num_fmt_OK, $num_OK) = (0, 0);
243	my ($bslash, $fcn, $sum, $fname, $rsp, $digest, $isOK);
244
245	local *FH;
246	$checkfile eq '-' and open(FH, '< -')
247		and $checkfile = 'standard input'
248	or sysopen(FH, $checkfile, O_RDONLY)
249		or die "sha3sum: $checkfile: $!\n";
250	while (<FH>) {
251		next if /^#/;
252		if (/^[ \t]*\\?(SHA3-|SHAKE)/) {
253			$modesym = '*';
254			($bslash, $fcn, $alg, $fname, $sum) =
255		/^[ \t]*(\\?)(SHA3-|SHAKE)(\d+) \((.+)\) = ([\da-fA-F]+)/;
256			if (defined $fcn and $fcn eq 'SHAKE') {
257				$alg *= 1000 if defined $alg;
258			}
259		}
260		else {
261			($bslash, $sum, $modesym, $fname) =
262			/^[ \t]*(\\?)([\da-fA-F]+)[ \t]([ *^U])(.+)/;
263			$alg = defined $sum ? $len2alg{length($sum)} : undef;
264		}
265		if (grep { ! defined $_ } ($alg, $sum, $modesym, $fname) or
266			! $isAlg{$alg}) {
267			warn("sha3sum: $checkfile: $.: improperly formatted" .
268				" SHA3 checksum line\n") if $warn;
269			$fmt_errs++;
270			$err = 1 if $strict;
271			next;
272		}
273		$num_fmt_OK++;
274		$fname = unescape($fname) if $bslash;
275		next if $ignore_missing && ! -e $fname;
276		$rsp = "$fname: ";
277		($binary, $text, $UNIVERSAL, $BITS) =
278			map { $_ eq $modesym } ('*', ' ', 'U', '^');
279		$isOK = 0;
280		unless ($digest = sumfile($fname)) {
281			$rsp .= "FAILED open or read\n";
282			$err = 1; $read_errs++;
283		}
284		elsif (lc($sum) eq $digest) {
285			$rsp .= "OK\n";
286			$isOK = 1;
287			$num_OK++;
288		}
289		else { $rsp .= "FAILED\n"; $err = 1; $match_errs++ }
290		print $rsp unless ($status || ($quiet && $isOK));
291	}
292	close(FH);
293	if (! $num_fmt_OK) {
294		warn("sha3sum: $checkfile: no properly formatted " .
295			"SHA3 checksum lines found\n");
296		$err = 1;
297	}
298	elsif (! $status) {
299		warn("sha3sum: WARNING: $fmt_errs line" . ($fmt_errs>1?
300		's are':' is') . " improperly formatted\n") if $fmt_errs;
301		warn("sha3sum: WARNING: $read_errs listed file" .
302		($read_errs>1?'s':'') . " could not be read\n") if $read_errs;
303		warn("sha3sum: WARNING: $match_errs computed checksum" .
304		($match_errs>1?'s':'') . " did NOT match\n") if $match_errs;
305	}
306	if ($ignore_missing && ! $num_OK && $num_fmt_OK) {
307		warn("sha3sum: $checkfile: no file was verified\n")
308			unless $status;
309		$err = 1;
310	}
311	return($err == 0);
312}
313
314
315	## Verify or compute SHA-3 checksums of requested files
316
317my($file, $digest);
318my $STATUS = 0;
319for $file (@ARGV) {
320	if ($check) { $STATUS = 1 unless verify($file) }
321	elsif ($digest = sumfile($file)) {
322		if ($file =~ /[\n\\]/) {
323			$file =~ s/\\/\\\\/g; $file =~ s/\n/\\n/g;
324			print "\\";
325		}
326		unless ($tag) { print "$digest $modesym$file\n" }
327		else          { print "$Tag{$alg} ($file) = $digest\n" }
328	}
329	else { $STATUS = 1 }
330}
331exit($STATUS);
332