bug fix to ensure to pick up proper entropy min/max values
[fwknop.git] / extras / spa-entropy / spa-entropy.pl
1 #!/usr/bin/perl -w
2 #
3 # File: spa-entropy.pl
4 #
5 # Purpose: To measure cross-packet SPA entropy on a byte by byte slice basis
6 #          and produce gunplot graphs.  This is useful to measure SPA packet
7 #          randomness after encryption.
8 #
9 # Author: Michael Rash <mbr@cipherdyne.org>
10 #
11 # License: GPL v2
12 #
13
14 use MIME::Base64;
15 use IPC::Open2;
16 use Getopt::Long 'GetOptions';
17 use strict;
18
19 my $use_ent = 1;
20 my $base64_decode = 1;
21 my $packets = 0;
22 my $prefix = 'entropy';
23 my $file_to_measure = '';
24 my $run_fwknop_client = 0;
25 my $min_len = 0;
26 my $lib_dir = '../../lib/.libs';
27 my $fwknop_client_path = '../../client/.libs/fwknop';
28 my $enc_mode = 'cbc';
29 my $enable_fwknop_client_gpg = 0;
30 my $spa_key_file = '../../test/local_spa.key';
31 my $help = 0;
32
33 my $use_openssl = 0;
34 my $openssl_salt = '0000000000000000';
35 my $openssl_mode = 'aes-256-cbc';
36
37 my %min_max_entropy = (
38     'min' => {
39         'val' => -1,
40         'pos' => 0,
41     },
42     'max' => {
43         'val' => -1,
44         'pos' => 0,
45     }
46 );
47
48 my @encrypted_data = ();
49 my @plaintext_data = ();
50 my @cross_pkt_data = ();
51
52 Getopt::Long::Configure('no_ignore_case');
53 die "[*] See '$0 -h' for usage information" unless (GetOptions(
54     'file-to-measure=s' => \$file_to_measure,
55     'base64-decode'     => \$base64_decode,
56     'count=i'           => \$packets,
57     'prefix=s'          => \$prefix,
58     'run-fwknop-client' => \$run_fwknop_client,
59     'enc-mode=s'        => \$enc_mode,
60     'gpg'               => \$enable_fwknop_client_gpg,
61     'lib-dir=s'         => \$lib_dir,
62     'Client-path=s'     => \$fwknop_client_path,
63     'use-openssl'       => \$use_openssl,
64     'openssl-salt=s'    => \$openssl_salt,
65     'openssl-mode=s'    => \$openssl_mode,
66     'help'              => \$help,
67 ));
68 &usage() if $help;
69
70 die "[*] Must execute --run-fwknop-client in --use-openssl mode"
71     if $use_openssl and not $run_fwknop_client;
72
73 &run_fwknop_client() if $run_fwknop_client;
74
75 &read_data();
76
77 &get_min_len();
78
79 &build_data_slices();
80
81 open F, "> $prefix.dat" or die $!;
82 my $pos = 0;
83 for my $str (@cross_pkt_data) {
84
85     my $entropy = &get_entropy($str);
86
87 #    print F "$pos $entropy\n";
88     print F "$pos $entropy   ### " . &hex_dump($str) . "\n";
89
90     if ($min_max_entropy{'min'}{'val'} == -1
91             and $min_max_entropy{'max'}{'val'} == -1) {
92         $min_max_entropy{'min'}{'val'} = $entropy;
93         $min_max_entropy{'min'}{'pos'} = $pos;
94         $min_max_entropy{'max'}{'val'} = $entropy;
95         $min_max_entropy{'max'}{'pos'} = $pos;
96     } else {
97         if ($entropy < $min_max_entropy{'min'}{'val'}) {
98             $min_max_entropy{'min'}{'val'} = $entropy;
99             $min_max_entropy{'min'}{'pos'} = $pos;
100         }
101         if ($entropy > $min_max_entropy{'max'}{'val'}) {
102             $min_max_entropy{'max'}{'val'} = $entropy;
103             $min_max_entropy{'max'}{'pos'} = $pos;
104         }
105     }
106     $pos++;
107 }
108 close F;
109
110 my $min = sprintf "%.2f", $min_max_entropy{'min'}{'val'};
111 my $max = sprintf "%.2f", $min_max_entropy{'max'}{'val'};
112
113 print "[+] Min entropy: $min at byte: $min_max_entropy{'min'}{'pos'}\n";
114 print "[+] Max entropy: $max at byte: $min_max_entropy{'max'}{'pos'}\n";
115
116 &run_gnuplot();
117
118 exit 0;
119
120 sub read_data() {
121
122     if ($use_openssl) {
123
124         ### we've already gotten plaintext information from the fwknop client,
125         ### so encrypt this data with openssl and use it to re-write the
126         ### $file_to_measure
127         unlink $file_to_measure if -e $file_to_measure;
128
129         my @openssl_encrypted_data = ();
130
131         ### encrypt the plaintext and use it to re-write the -f file
132         for my $line (@plaintext_data) {
133
134             my $ptext_file = 'ptext.tmp';
135             my $enc_file   = 'ptext.enc';
136
137             open F, "> $ptext_file" or die $!;
138             print F $line;
139             close F;
140
141             unlink $enc_file if -e $enc_file;
142
143             system "openssl enc -$openssl_mode -a -S $openssl_salt " .
144                 "-in ptext.tmp -out ptext.enc -k fwknoptest000000";
145
146             my $base64_enc_data = '';
147             open F, "< $enc_file" or die $!;
148             while (<F>) {
149                 chomp;
150                 $base64_enc_data .= $_;
151             }
152             close F;
153
154             push @openssl_encrypted_data, $base64_enc_data;
155
156         }
157
158         open F, "> $file_to_measure" or die $!;
159         for my $line (@openssl_encrypted_data) {
160             print F $line, "\n";
161         }
162         close F;
163     }
164
165     my $fh = *STDIN;
166     if ($file_to_measure) {
167         open IN, "< $file_to_measure" or die "[*] Could not open $file_to_measure: $!";
168         $fh = *IN;
169     }
170
171     my $l_ctr = 0;
172     while (<$fh>) {
173         next unless $_ =~ /\S/;
174         chomp;
175
176         if ($base64_decode) {
177             if (&is_base64($_)) {
178                 my $base64_str = $_;
179
180                 if ($enable_fwknop_client_gpg) {
181                     unless ($base64_str =~ /^hQ/) {
182                         $base64_str = 'hQ' . $base64_str;
183                     }
184                 } else {
185                     ### base64-encoded "Salted__" prefix
186                     unless ($base64_str =~ /^U2FsdGVkX1/) {
187                         $base64_str = 'U2FsdGVkX1' . $base64_str;
188                     }
189                 }
190
191                 my ($equals_rv, $equals_padding) = &base64_equals_padding($base64_str);
192                 if ($equals_padding) {
193                     $base64_str .= $equals_padding;
194                 }
195                 my $str = decode_base64($base64_str);
196
197                 if ($enable_fwknop_client_gpg) {
198                     $str =~ s/^\x85\x02//;
199                 } else {
200                     $str =~ s/^Salted__//;
201                 }
202                 push @encrypted_data, $str;
203             } else {
204                 push @encrypted_data, $_;
205             }
206         } else {
207             push @encrypted_data, $_;
208         }
209
210         $l_ctr++;
211         if ($packets > 0) {
212             last if $l_ctr == $packets;
213         }
214     }
215
216     ### hex dump encrypted data
217     open HEX, "> hex_dump.data" or die $!;
218     for my $line (@encrypted_data) {
219         print HEX &hex_dump($line), "\n";
220     }
221     close HEX;
222
223     print "[+] Read in $l_ctr SPA packets...\n";
224     return;
225 }
226
227 sub run_fwknop_client() {
228     die "[*] Must set packets file with -f <file>" unless $file_to_measure;
229     die "[*] Must set packet count with -c <count>" unless $packets;
230
231     if (-e $file_to_measure) {
232         unlink $file_to_measure or die $!;
233     }
234
235     my $cmd = "LD_LIBRARY_PATH=$lib_dir $fwknop_client_path -A tcp/22 " .
236         "-a 127.0.0.2 -D 127.0.0.1 --get-key $spa_key_file " .
237         "-B $file_to_measure -b -v --test";
238
239     if ($enable_fwknop_client_gpg) {
240         $cmd .= ' --gpg-recipient-key 361BBAD4 --gpg-signer-key 6A3FAD56 ' .
241             '--gpg-home-dir ../../test/conf/client-gpg';
242     } else {
243         $cmd .= " -M $enc_mode";
244     }
245     $cmd .= " 2> /dev/null";
246
247     print "[+] Running fwknop client via the following command:\n\n$cmd\n\n";
248
249     for (my $i=0; $i < $packets; $i++) {
250         open C, "$cmd |" or die $!;
251         while (<C>) {
252             if (/Plaintext\:\s+(\S+)/) {
253                 push @plaintext_data, $1;
254                 last;
255             }
256         }
257         close C;
258     }
259
260     return;
261 }
262
263 sub get_min_len() {
264
265     ### calculate minimum length
266     for my $line (@encrypted_data) {
267         chomp $line;
268         next unless $line =~ /\S/;
269         my $len = length($line);
270         if ($min_len == 0) {
271             $min_len = $len;
272         } else {
273             if ($len < $min_len) {
274                 $min_len = $len;
275             }
276         }
277     }
278     return;
279 }
280
281 sub build_data_slices() {
282     for my $line (@encrypted_data) {
283         my @chars = split //, $line;
284         my $c_ctr = 0;
285         for my $char (@chars) {
286             $cross_pkt_data[$c_ctr] .= $char;
287             last if $c_ctr == $min_len;
288             $c_ctr++;
289         }
290     }
291     return;
292 }
293
294 sub run_gnuplot() {
295     open F, "> $prefix.gnu" or die $!;
296
297     my $enc_str = $enc_mode;
298     $enc_str = 'gpg' if $enable_fwknop_client_gpg;
299
300     my $yrange = '[0:9]';
301     print F <<_GNUPLOT_;
302 set title "SPA slice entropy (encryption mode: $enc_str)"
303 set terminal gif nocrop enhanced
304 set output "$prefix.gif"
305 set grid
306 set yrange $yrange
307 plot '$prefix.dat' using 1:2 with lines title 'min: $min \\@ byte: $min_max_entropy{'min'}{'pos'}, max: $max \\@ byte: $min_max_entropy{'max'}{'pos'}'
308 _GNUPLOT_
309     close F;
310
311     print "[+] Creating $prefix.gif gnuplot graph...\n\n";
312     system "gnuplot $prefix.gnu";
313
314     return;
315 }
316
317 sub get_entropy() {
318     my $data = shift;
319
320     my $entropy = '';
321
322     my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'ent');
323
324     print CHLD_IN $data;
325     close CHLD_IN;
326
327     while (<CHLD_OUT>) {
328         ### Entropy = 5.637677 bits per byte.
329         if (/Entropy\s=\s(\d\S+)/) {
330             $entropy = $1;
331             last;
332         }
333     }
334
335     close CHLD_OUT;
336
337     waitpid $pid, 0;
338     my $child_exit_status = $? >> 8;
339
340     return $entropy;
341 }
342
343 sub base64_equals_padding() {
344     my $msg = shift;
345     my $padding = '';
346
347     return 1, $padding if $msg =~ /=$/;
348
349     my $remainder = 4 - length($msg) % 4;
350
351     if ($remainder == 3) {
352         ### not possible for valid base64 data - should only have
353         ### pad with one or two '=' chars
354         return 0, $padding;
355     }
356
357     unless ($remainder == 4) {
358         $padding .= '='x$remainder;
359     }
360     return 1, $padding;
361 }
362
363 sub hex_dump() {
364     my $data = shift;
365
366     my @chars = split //, $data;
367     my $ctr = 0;
368
369     my $hex_part   = '';
370     my $ascii_part = '';
371
372     for my $char (@chars) {
373
374         $hex_part .= sprintf "%.2x", ord($char);
375
376         if ($char =~ /[^\x20-\x7e]/) {
377             $ascii_part .= '.';
378         } else {
379             $ascii_part .= $char;
380         }
381         $ctr++;
382     }
383     return "$hex_part $ascii_part";
384 #    return "$ascii_part";
385 }
386
387 sub is_base64() {
388     my $data = shift;
389
390     ### check to make sure the packet data only contains base64 encoded
391     ### characters per RFC 3548:   0-9, A-Z, a-z, +, /, =
392     if ($data =~ /[^\x30-\x39\x41-\x5a\x61-\x7a\x2b\x2f\x3d]/) {
393         return 0;
394     }
395     if ($data =~ /=[^=]/) {
396         return 0;
397     }
398     return 1;
399 }
400
401 sub usage() {
402     print "$0 [options]\n";
403     exit 0;
404 }