この記事は後半です。前半は Qiita にあります: AES-256のはずが9ビットで足りる——MuPDFの暗号化PDFが軽く破られる背景
![]()
画像: en.wikipedia.org より
前半で見た通り、問題の出発点は drand48 系の 48 ビット LCG でした。仕様上は 48 ビット状態を持ちますが、MuPDF では seed に Unix 時刻の秒が使われ、状態の低 16 ビットは固定値 0x330e です。つまり、時刻で揺れるのは上位 32 ビットだけです。
ところが、鍵生成ではこの状態をそのまま使わず、lrand48() で順に byte を抜いていました。LCG の掛け算と足し算では、上位ビットの揺れは下位へまったく伝わりません。MuPDF は lrand48() の戻り値を 1 バイトに切り詰めて使っていたので、鍵に入るのは状態のビット 17〜24 だけで、ビット 25 以上は鍵に効きません。そこに固定部分を差し引くと、時刻の 32 ビット全部は残らず、実質 9 ビットだけが鍵の違いを作る、という構図になります。
この 9 ビットが意味するのは、鍵の候補が 512 通りしかないことです。AES-256 の見た目はそのままでも、実際には「あり得る鍵の集合」が非常に小さい。暗号の強さを決めるのはアルゴリズム名ではなく、最終的に何通りを試せるかです。
V12 の検証では、512 通りの候補を純粋に試すだけで約 11 ミリ秒だったと報告されています。ここで速さを支えているのは、重いことをしていない点です。パスワードの総当たりではなく、まず鍵候補を作る。次に、それが正しいかを短い照合で確かめる。MuPDF は同じ乱数系列を /ID にも使っていたので、トレーラに平文である /ID を追加の手がかりにできます。
つまり、候補ごとに PDF 全体を復号して長い検証をする必要がありません。乱数系列そのものの一致を見ればよい。だから 512 通りは「少ない」だけでなく、「検証コストも軽い」。この二重の軽さが、秒単位どころかミリ秒単位にまで縮めています。
ただし、11 ミリ秒という数値は環境依存です。CPU、実装、並列化、対象文書の状態で変わります。ここで大事なのは絶対値よりも、候補空間が小さすぎて現実的に一瞬で終わるという事実です。
この種の問題は、攻撃者視点では「見つけたら終わり」の脆弱性です。一方で、実務での教訓はもう少し広い。PDF を出力する側が暗号化をオンにしても、それだけでは安全になりません。ライブラリがどの乱数を使い、何を seed にしているかまで見ないと、暗号化したつもりの文書が実質的に薄い膜になります。
自分なら、社内ツールや帳票生成で PDF 暗号化を使うとき、まずライブラリ任せの乱数実装を前提にしません。暗号化の「有効化」だけを要件にせず、鍵導出の方式と乱数源を確認します。もし古い実装を避けられないなら、少なくとも「誰が、どの時点で、どんな乱数で鍵を作るのか」をレビュー対象に入れます。ここを抜かすと、AES-256 というラベルだけが残ります。
MuPDF が後に ChaCha20 ベースの暗号学的 PRNG に置き換えたのは、筋のよい修正です。問題の核心は AES でも PDF 仕様そのものでもなく、鍵材料を生む入口が予測しやすかったことにあります。実際、V12 の調査でも、同種の脆さは MuPDF だけでなく、Foxit、OpenPDF、iText の系譜にも見つかっています。
だから、この話を「AES-256 でも危ない」で終えると少し浅いです。正確には、強い暗号を使っても、入口が弱ければ全体は弱い。PDF の暗号化は、仕様・実装・乱数・保存形式が絡むので、どこか一つが甘いだけでも破綻します。MuPDF の件は、その破綻が 512 通りという極端な形で露出した例でした。