pdfのstreamオブジェクトはパスワードかかってなくても暗号化されているケースがある


少し要因が見えてきたので、タイトル変更しました。
→解決しました。
結論は追記およびコメント参照ください。

経緯

とあるpdfファイルからテキストを抽出できないか 1 調べていたところ
<<.../FlateDecode...>stream...endstreamのデータを復号できずに困り、pdfフォーマットの解説書を読んだが、そのファイル自身が、自ら定義しているフォーマットに違反しているようにしか見えなかった。

pdfフォーマットの解説書

https://www.adobe.com/devnet/pdf/pdf_reference.html 内のリンク
Document Management – Portable Document Format – Part 1: PDF 1.7, First Edition - PDF32000_2008.pdf

RFC1950 (とRFC1951)に準拠しているように読み取れる。

該当のPDFのデータ

"stream" 2 以降の9A FC~が、データであり、zlibフォーマットに従っているはずだが、

RFCs 1950によると

  • 先頭から2byteがCMF(1byte),FLG(1byte)の順で配置され、CMFの下位4bit(Compresstion method)は、値8しか定義されていない。(ただ、使うなとは書いていない。)

  • 先頭から2byteをビッグエンディアンの数値としてみたときに、31で割り切れること

The FCHECK value must be such that CMF and FLG, when viewed as
a 16-bit unsigned integer stored in MSB order (CMF*256 + FLG),
is a multiple of 31.

とあるが、下記のよるに、0x9AFC(39676)は31では割り切れない。(余り:27)

であり、準拠しているように見えない。
どうやって復号すりゃいいんだ・・。へるぷみー
とりあえずJavaで試してみたが復号できない。
ちなみにC#で先頭2byte捨ててDeflateStreamで読み込む方法もダメでした。

要因→追記参照

試してみたDeflate復号用のソースコード(Java)

78 9Cとかで始まっているデータで動確済み。

ソースコード
DeflateTest.java

import java.io.*;
import java.util.zip.*; // to use Inflater

public class DeflateTest
{
    public static byte[] GetBytes(String path, long srcOffset, int srcLength) throws IOException
    {
        byte[] b = new byte[srcLength];
        int n = 0;

        FileInputStream fs = null;

        try {
            fs = new FileInputStream(path);

            fs.skip(srcOffset);
            n = fs.read(b, 0, srcLength);
            if (n != srcLength) {
                b = null;
            }
        }
        catch (FileNotFoundException e) {
            System.out.println(e);
            return null;
        }
        catch (IOException e) {
            System.out.println(e);
            return null;
        }
        finally {
            if (fs != null) {
                fs.close();
            }
        }
        return b;
    }


    public static void DecodeFile(String path, long srcOffset, int srcLength) throws IOException
    {
        byte[] buf = GetBytes(path, srcOffset, srcLength);
        if ( buf == null ) {
            return;
        }

        Inflater inf = new Inflater();

        inf.setInput(buf, 0, srcLength);
        byte[] tmp = new byte[1024];

        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("test_out.dat");

            while ( !inf.finished() ) {
                int len = inf.inflate(tmp);
                if ( len > 0)  {
                    fos.write(tmp, 0, len);
                }
            }
        }
        catch (DataFormatException e) {
            System.out.println(e);
        }
        finally {
            if ( fos != null ) {
                fos.close();
            }
        }
    }

    public static void main(String[] args) throws IOException
    {
        if (args.length != 3) {
            System.out.println("Parameter error.");
            return;
        }

        long offset = 0;
        int length = 0;
        try {
            offset = Long.decode(args[1]);
            length = Integer.decode(args[2]);
        }
        catch (Exception e) {
            System.out.println("Parameter parse error.");
            return;
        }
        DecodeFile(args[0], offset, length);
    }
}

コンパイル方法:javac DeflateTest.java
実行方法:java DeflateTest ファイルパス 開始バイト位置 バイト長

実行結果

>java DeflateTest PDF32000_2008.pdf 0x434A 542
java.util.zip.DataFormatException: incorrect header check

追記

streamが暗号化されている

保護されたpdfだと事前に別のデコード処理が要るとかか→あたりっぽい。
(pdf最後尾(trailer)に/Encryptがあるかどうかで判断できそう。)

7.6.2章の最後のほうに下記の記載がある。まさにこれ。

AESの場合は、さらに、streamの先頭16byte分にAES CBC modeのinitial vectorが配置されているらしい。

下記が非常に参考になりました。
pdfの暗号化 - https://qiita.com/mtgto/items/e54fb19d0547590ca791

差分エンコーディング?

さらに差分エンコーディングしている可能性がある。不明。
/Decodeparms, /Predictor(PLRMを参照)が関係しそう?

http://7shi.hateblo.jp/entry/20110207/1297053867

今回は関係なかった。


  1. Word2016でpdfを開くと、表とかも含めてWord形式にコンバートできる。が、ファイルや環境によってはデータが破損したりして使えないケースがある。 

  2. 直後の改行コード\r(0D)または\n(0A)または\r\n(0D 0A)は無視される