#!/usr/bin/env perl require 5.008; use warnings; use strict; unshift(@INC, '.'); require qpdf_test_helpers; chdir("qpdf") or die "chdir testdir failed: $!\n"; require TestDriver; cleanup(); my $td = new TestDriver('unicode-password'); my $n_tests = 0; # $n_tests incremented below # Files with each of these passwords when properly encoded have been # tested manually with multiple PDF viewers. Adobe Reader, chrome, # xpdf, and gv can open all of them except R3 with "single-byte", # which can be opened by xpdf and gv but not the others. As of # 2019-01-19, okular and atril (evince) are not able to open R=6 files # with Unicode passwords as generated by qpdf but can open the R=3 # files. # [bits, password-or-password-name, write-encoding, actual-encoding, xargs, # [[read-encoding, strict?, fail?, tried-others, xargs]]] my @unicode_pw_cases = ( [128, 'simple', 'pdf-doc', 'pdf-doc', '', [['utf8', 0, 0, 1, ''], ['utf8', 1, 1, 0, ''], ['pdf-doc', 1, 0, 0, ''], ]], [128, 'simple', 'utf8', 'utf8', '--password-mode=bytes', [['pdf-doc', 0, 0, 1, ''], ['pdf-doc', 1, 1, 0, ''], ['utf8', 1, 0, 0, ''], ]], [128, 'simple', 'utf8', 'pdf-doc', '--password-mode=unicode', [['pdf-doc', 1, 0, 0, ''], ]], [128, 'simple', 'utf8', 'pdf-doc', '--password-mode=auto', [['pdf-doc', 1, 0, 0, ''], ]], [128, 'single-byte', 'utf8', 'pdf-doc', '', [['pdf-doc', 1, 0, 0, ''], ['win-ansi', 0, 0, 1, ''], ]], [128, 'single-byte', 'utf8', 'pdf-doc', '--password-mode=unicode', [['pdf-doc', 1, 0, 0, ''], ['win-ansi', 0, 0, 1, ''], ]], [128, 'single-byte', 'win-ansi', '', '--password-mode=unicode', "supplied password is not valid UTF-8\n", ], [128, 'single-byte', 'win-ansi', 'win-ansi', '', [['win-ansi', 1, 0, 0, ''], ]], [128, 'single-byte', 'pdf-doc', 'pdf-doc', '', [['pdf-doc', 1, 0, 0, ''], ['win-ansi', 0, 0, 1, ''], ['pdf-doc-hex', 1, 0, 0, '--password-mode=hex-bytes'], ]], [128, 'complex', 'utf8', '', '--password-mode=unicode', "supplied password cannot be encoded for 40-bit or" . " 128-bit encryption formats\n" ], [128, 'complex', 'utf8', 'utf8', '--password-mode=bytes', [['utf8', 1, 0, 0, ''], ]], [256, 'single-byte', 'win-ansi', '', '--password-mode=unicode', "supplied password is not valid UTF-8\n", ], [256, 'single-byte', 'win-ansi', '', '--password-mode=auto', "supplied password is not a valid Unicode password, which is" . " required for 256-bit encryption; to really use this password," . " rerun with the --password-mode=bytes option\n", ], [256, 'single-byte', 'win-ansi', 'win-ansi', '--password-mode=bytes', [['utf8', 0, 0, 1, ''], ['utf8', 1, 1, 0, ''], ['win-ansi', 1, 0, 0, ''], ['win-ansi', 0, 0, 0, ''], ['pdf-doc', 0, 0, 1, ''], ['pdf-doc-hex', 0, 0, 1, '--password-mode=hex-bytes'], ]], [256, 'complex', 'utf8', 'utf8', '', [['utf8', 1, 0, 0, ''], ['utf8-hex', 1, 0, 0, '--password-mode=hex-bytes'], ]], [256, 'complex', 'utf8-hex', 'utf8', '--password-mode=hex-bytes', [['utf8', 1, 0, 0, ''], ['utf8-hex', 1, 0, 0, '--password-mode=hex-bytes'], ]], [256, 'complex', 'utf8', 'utf8', '--password-mode=unicode', [['utf8', 1, 0, 0, ''], ['password-arg-simple-utf8', 0, 1, 1, ''], ]], ); for my $d (@unicode_pw_cases) { my $decode_cases = $d->[5]; $n_tests += 1; if (ref($decode_cases) eq 'ARRAY') { $n_tests += scalar(@$decode_cases); } } foreach my $d (@unicode_pw_cases) { my ($bits, $pw, $w_encoding, $a_encoding, $xargs, $decode_cases) = @$d; my $w_pfile = "password-bare-$pw-$w_encoding"; my $upass; if (-f $w_pfile) { $upass = '@' . $w_pfile; } else { $upass = "$pw"; } my $outbase = "unicode-pw-$bits-$pw-$w_encoding-$xargs"; my $exp = ''; if (ref($decode_cases) ne 'ARRAY') { $exp = "qpdf: $decode_cases"; $decode_cases = []; } $td->runtest("encode $bits, $pw, $w_encoding", {$td->COMMAND => "qpdf $xargs --static-id --static-aes-iv" . " --allow-weak-crypto" . " --encrypt $upass o $bits -- minimal.pdf a.pdf"}, {$td->STRING => $exp, $td->EXIT_STATUS => ($exp ? 2 : 0)}, $td->NORMALIZE_NEWLINES); foreach my $d2 (@$decode_cases) { my ($r_encoding, $strict, $xfail, $tried_others, $r_xargs) = @$d2; my $r_pfile = "password-arg-$pw-$r_encoding"; if (! -f $r_pfile) { $r_pfile = $r_encoding; } my $r_output = ""; $r_output .= "trying other\n" if $tried_others; my $arg = "--show-encryption"; if ($xfail) { $r_output .= "qpdf: a.pdf: invalid password\n"; $arg = "--check"; } else { $r_output .= "R = " . ($bits == 128 ? '3' : '6') . "\n"; open(F, "); close(F); $r_output .= "User password = $apw\n"; } $r_xargs .= $strict ? ' --suppress-password-recovery' : ''; $td->runtest("decrypt $pw, $r_encoding, strict=$strict", {$td->COMMAND => "qpdf $arg --verbose" . " $r_xargs a.pdf \@$r_pfile", $td->FILTER => "perl show-unicode-encryption.pl"}, {$td->STRING => "$r_output", $td->EXIT_STATUS => ($xfail ? 2 : 0)}, $td->NORMALIZE_NEWLINES); } } $n_tests += 5; $td->runtest("bytes fallback warning", {$td->COMMAND => "qpdf --allow-weak-crypto" . " --encrypt \@password-bare-complex-utf8 o 128 --" . " minimal.pdf a.pdf"}, {$td->FILE => "bytes-fallback.out", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); { # local scope my $r_output = ""; $r_output .= "R = 3\n"; open(F, "); close(F); $r_output .= "User password = $apw\n"; $td->runtest("decrypt bytes fallback", {$td->COMMAND => "qpdf --show-encryption --verbose" . " a.pdf \@password-arg-complex-utf8" . " --password-mode=bytes", $td->FILTER => "perl show-unicode-encryption.pl"}, {$td->STRING => "$r_output", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); } # Exercise passing Unicode passwords via the command line. This tests # wmain for Windows and assumes a UTF-8 locale for other platforms. $td->runtest("Unicode at CLI", {$td->COMMAND => "qpdf --encrypt π ʬ 256 --" . " minimal.pdf a.pdf"}, {$td->STRING => "", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); $td->runtest("Decrypt using user password", {$td->COMMAND => "qpdf --show-encryption a.pdf --password=π"}, {$td->FILE => "unicode-up.out", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); $td->runtest("Decrypt using owner password", {$td->COMMAND => "qpdf --show-encryption a.pdf --password=ʬ"}, {$td->FILE => "unicode-op.out", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); cleanup(); $td->report($n_tests);