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

少し要因が見えてきたので、タイトル変更しました。
→解決しました。
結論は[追記](https://qiita.com/kob58im/items/89d4e67e062b3ae5177d#%E8%BF%BD%E8%A8%98)およびコメント参照ください。

# 経緯
とある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](https://www.adobe.com/content/dam/acom/en/devnet/pdf/PDF32000_2008.pdf) - PDF32000_2008.pdf

> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/38ae3612-425c-bd9c-20e8-ed26fe3c8a6a.png)

[RFC1950](https://www.ietf.org/rfc/rfc1950.txt) (とRFC1951)に準拠しているように読み取れる。

# 該当のPDFのデータ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/8c64c908-f4b9-0850-bed7-841fa381f2b5.png)

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

[RFCs 1950](https://www.ietf.org/rfc/rfc1950.txt)によると

- 先頭から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`で読み込む方法もダメでした。~~

要因→[追記参照](https://qiita.com/kob58im/items/89d4e67e062b3ae5177d#%E8%BF%BD%E8%A8%98)

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

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

<details><summary>ソースコード</summary><div>

```java: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);
    }
}

```

</div></details>

コンパイル方法：`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章の最後のほうに下記の記載がある。<b>まさにこれ。</b>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/bb5525ba-a155-486b-2ad0-7c8e88b3cb5b.png)

AESの場合は、さらに、streamの先頭16byte分にAES CBC modeのinitial vectorが配置されているらしい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/ba354d52-64a1-dd1f-4835-27146ddaf3a2.png)


下記が非常に参考になりました。
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`)は無視される