HotXLSは、Excel 2010以降のすべてのバージョンでデフォルトで適用されるパスワード保護形式であるAgile暗号化されたExcelファイルを、TXLSXWorkbook.OpenEncryptedという1回の呼び出しで読み取ります。このコンポーネントはXML暗号化記述子を解析し、SHA-512スピンカウントハッシュチェーンを用いてパスワードから鍵を派生させ、暗号化された検証子とパスワードを照合し、その後、パッケージを4096バイトのAES-CBCセグメント単位で復号します。ExcelのインストールやCOM、外部の暗号化DLLは一切不要です
本記事では、特にAgile暗号化の読み取り側に焦点を当てて解説します。関連する2つの課題についてはそれぞれの個別記事があります。古いBIFF .xlsファイル内のレガシーなRC4およびXOR方式との相互運用については、ECBおよびRC4相互運用の記事で説明しており、ECMA-376標準暗号化によるパスワード保護されたワークブックの作成については、AES保護されたXLSX出力の記事でカバーしています。本記事では、ファイルがすでに存在し、他者が暗号化したものを読み取るタスクを扱います
この課題が必要となる状況は、ドキュメントパイプラインを運用している開発者にとってはおなじみのものです。サーバー側のインポートサービスがワークブックのアップロードを受け取るとします。そのマシンにはExcelがインストールされておらず、今後もインストールされる予定はありません。ある朝、顧客が極めて一般的な.xlsxをアップロードしましたが、ZIPリーダーが「ZIPファイルではない」として拒否します。顧客はファイルをパスワード付きで保存していました。その瞬間から、開発者が作成したローダーが[MS-OFFCRYPTO]を理解するか、あるいはユーザーにとっては「何も珍しいことはしていない」ファイルを突き返すかの二択を迫られることになります
ExcelファイルにおけるAgile暗号化とは何か?
Agile暗号化は、[MS-OFFCRYPTO] §2.3.4.10から§2.3.4.15で定義されているパスワード保護方式であり、Excel 2010以降でワークブックをパスワード付きで保存した際に書き込まれます。暗号化されたファイルは、もはや通常のZIPパッケージではありません。これはOLE Compound File Binary(CFB)コンテナであり、暗号化の実行方法を記述したEncryptionInfoと、暗号化された不透明なブロブである実際の.xlsx ZIPデータを保持するEncryptedPackageという2つのストリームを含んでいます。CFBのシグネチャ(D0 CF 11 E0 A1 B1 1A E1)は、古い形式であるBIFF .xlsファイルが持つマジックナンバーと同じであるため、名前が変更されたファイルや暗号化されたファイルを拡張子だけで分類することはできません
Agile暗号化をそれ以前の方式と区別する特徴は、EncryptionInfoが自己記述型である点です。メジャーバージョンとマイナーバージョンがともに4である8バイトのバージョン接頭辞の後に、ストリームはUTF-8エンコードされたXML記述子として続きます。keyData要素は、暗号アルゴリズム(AES)、チェーニングモード(ChainingModeCBC)、ハッシュ関数(SHA512)、ビット単位の鍵長、ブロックサイズ、およびBase64エンコードされたソルトを宣言します。パスワードのkeyEncryptor要素は、独自のソルト、spinCount、および3つのBase64ペイロード(encryptedVerifierHashInput、encryptedVerifierHashValue、encryptedKeyValue)を保持します。Excelはスピンカウント100,000でAES-256を書き込みますが、記述子ではAES-128 or AES-192を宣言することも許可されており、HotXLSは256であると仮定せずkeyBitsの宣言値を尊重します
プレーンテキスト、標準、およびAgileワークブック用の単一エントリーポイント
TXLSXWorkbook.OpenEncryptedは、呼び出し側が遭遇する可能性のある3つの状態(通常のZIP、標準暗号化、およびAgile暗号化)のすべてを処理するため、アップロードハンドラはロード前にファイルを分類する必要がありません。このメソッドはまずファイルを検査します。CFBシグネチャがない場合は通常のOpenパスに処理を委ね、パスワードは無視されます。ファイルがCFBコンテナである場合は、まずECMA-376標準暗号化を試し、EncryptionInfoのバージョンシグネチャがAgile 4.4である場合にはAgileパイプラインに振り分けます。戻り値は成功時に1であり、これはOpenメソッドと同じ動作契約です
var
Wb: TXLSXWorkbook;
begin
Wb := TXLSXWorkbook.Create;
try
// Works for plain .xlsx, Standard-encrypted and
// Agile-encrypted files alike
if Wb.OpenEncrypted('upload.xlsx', 'customer-password') = 1 then
Writeln(VarToWideStr(Wb.Sheets[1].Cells[1, 1].Value));
finally
Wb.Free;
end;
end;
暗号化されていない入力に対するフォールバックは、見た目以上に重要です。常にOpenEncryptedを呼び出すバッチインポーターは、呼び出し側での分岐処理を必要としません。保護されていないファイルは以前と全く同様にロードされ、暗号化されて届いたファイルはその場で復号され、インメモリのストリームとして通常のZIPローダーに渡されます。テストすべきコードパスは3つではなく1つだけになります
パスワードはどのようにしてAES鍵になるのか?
Agile暗号化では、パスワードが直接使用されることはありません。HotXLSはまず反復ハッシュを計算します。初期ダイジェストは、パスワードのソルトとパスワードのUTF-16LEバイトデータを結合したものに対するSHA-512ハッシュ値です。その後、ダイジェストはspinCount回再ハッシュされ、各ラウンドにおいて前のダイジェストの前に32ビットのリトルエンディアンで表現された反復カウンタが追加されます。Excelのデフォルトのスピンカウントである100,000回では、パスワード試行1回につき10万回連続してSHA-512が呼び出されますが、これこそが設計の目的です。スピンカウントは総当たり攻撃に対するスロットル(制限)として機能します。正当な呼び出し側にとっては1回あたり数ミリ秒のコストですが、辞書攻撃者にとっては推測を1回試みるたびに毎回同じ数ミリ秒のコストがかかることになります
// [MS-OFFCRYPTO] iterated password hash:
// H(0) = SHA-512(salt + UTF-16LE(password))
// H(n) = SHA-512(LE32(n - 1) + H(n - 1)), repeated spinCount times
function AgilePasswordHash(const Password: WideString;
const Salt: TBytes; SpinCount: Integer): TBytes;
var
buf: TBytes;
i: Integer;
begin
Result := XlsSHA512(Concat(Salt, Utf16LEBytes(Password)));
SetLength(buf, 4 + 64);
for i := 0 to SpinCount - 1 do
begin
PutLE32(buf, 0, i); // iteration counter, little-endian
Move(Result[0], buf[4], 64); // previous digest
Result := XlsSHA512(buf);
end;
end;
spun hashは依然として鍵ではありません。そこからさらに、特定の目的ごとに固定された8バイトのブロックキーを結合してもう一度ハッシュ計算を行うことで、3つの個別の鍵が派生します。検証子入力の復号にはFE A7 D2 76 3B 4B 9E 79、検証子ハッシュにはD7 AA 0F 6D 30 61 34 4E、そして実際のパッケージ鍵の復号には14 6E 0B E7 AB AC D0 D6が使用されます。各SHA-512結果は宣言された鍵長に切り詰められ、[MS-OFFCRYPTO]の規定に従い、ハッシュが鍵より短い場合は0x36バイトでパディングされます。同じ0x36パディングルールは、CBCの初期化ベクトルとして使用するためにパスワードソルトをブロックサイズに拡張する際にも適用されます
パスワードの検証とsaltSizeの切り詰めに関する罠
HotXLSは、記述子から得られた検証子のペアを使用して、パッケージデータに触れる前にパスワードを検証します。最初の派生鍵でencryptedVerifierHashInputを復号し、その結果のSHA-512ハッシュ値を計算し、2番目の派生鍵でencryptedVerifierHashValueを復号し、2つのダイジェストをバイト単位で比較します。一致しない場合はパスワードが誤っていることを意味し、文字化けしたワークブックが生成されるのではなく明確なエラー結果として報告されます。重要な点として、誤った鍵でパッケージ本体が復号されることは絶対にないため、誤ったパスワードによって一見正常に見える破損データが生成される心配はありません
EncryptedPackageはどのように復号されるのか?
EncryptedPackageストリームは、8バイトのリトルエンディアンで表現されたプレーンテキストサイズで始まり、その後に4096バイトのセグメント単位の暗号文が続きます。HotXLSはセグメントごとに新しい初期化ベクトル(IV)を用いて順次復号します。パッケージ鍵自体はパスワードから直接生成されるのではなく、書き込みツールがencryptedKeyValueに暗号化したランダムな中間鍵であり、HotXLSは3番目の派生鍵でこれを復号し、keyDataが宣言する鍵長で切り詰めます。各セグメントのIVは、keyDataのソルトと32ビットのリトルエンディアンで表現されたセグメントインデックスを結合したデータに対するSHA-512ハッシュ値を計算し、それをブロックサイズに切り詰めたものです。この構造により、任意の4096バイトセグメントを独立して復号できるため、原理的にはランダムアクセスに適した形式となっています。ただし、HotXLSはパッケージ全体をメモリ上に復号し、得られたZIPバイトデータを通常のXLSXローダーに渡します
宣言されたプレーンテキストサイズは、最後の処理で重要な役割を果たします。AES-CBCの出力はブロック配置されるため、最後のセグメントにはドキュメントデータの一部ではない最大15バイトのパディングが含まれます。復号されたバッファはサイズ接頭辞に従って切り詰められ、その結果、Excelが暗号化した正確な.xlsx ZIPデータが得られます。HotXLSは復号前にこのサイズ接頭辞と実際のストリーム長を照合するため、アップロードデータが途中で切れている場合やサイズフィールドが改ざんされている場合、バッファオーバーランを起こすことなく正常にエラーとなります
エラー報告と対応範囲の境界
エラーの発生モードは、意図的に分離されています。パスワードが誤っている場合は検証子の不一致により、明示的なパスワードエラーメッセージを含む例外が発生し、UIでユーザーに再試行を促すことができます。一方、サポート対象外のアルゴリズム(Agile記述子においてAES/CBC/SHA-512以外の組み合わせ)が宣言されているCFBコンテナや、標準暗号化でもAgile暗号化でもないコンテナの場合は、その方式がサポートされていないことを示す別の例外が発生します。この2つを決して混同してはなりません。サポートされていない暗号方式に対してパスワードを再試行させることはユーザーの時間を無駄にし、誤ったパスワードをファイル形式のエラーとして報告することはサポートチームを混乱させるためです
function LoadUploadedWorkbook(const FileName: WideString;
const Password: WideString; Wb: TXLSXWorkbook): Boolean;
begin
Result := False;
try
Result := Wb.OpenEncrypted(FileName, Password) = 1;
except
on E: EXlsxEncryptionNotImplemented do
// Raised for both a wrong password and an unsupported
// scheme; E.Message states which, so log it verbatim and
// only offer a password retry for the wrong-password case
RejectUpload(FileName, E.Message);
end;
end;
対応範囲の境界について明確にしておきます。HotXLSは、SHA-512ハッシュとCBCモードのAESを宣言するAgile記述子を読み取ることができ、これはExcel 2010からExcel 365が実際に書き込むすべての鍵サイズ(128、192、256ビット)をカバーしています。他の暗号アルゴリズムやハッシュ関数を宣言する記述子は、処理を推測することなく拒否され、証明書ベースの鍵暗号化ツールは参照されず、パスワードによる鍵暗号化ツールのみが使用されます。書き込み側について、HotXLSは現在、Agileではなく標準暗号化を生成します。この違いは、後続のツールが暗号化方式を検査する場合に重要となります。詳細については、AES保護されたXLSX出力の書き込みに関する記事に掲載されています
パスワード保護されたアップロードファイルは、ローダーが暗号化をエラーではなくファイル形式の一部として扱うようになれば、特別なケースではなくなります。ここで説明したOpenEncryptedエントリポイント、SHA-512スピンカウント派生、およびセグメント化されたAES-CBCパイプラインは、DelphiおよびC++Builder向けのネイティブXLSおよびXLSXの読み書きエンジンとともに、HotXLS Delphi Excel Componentの一部として提供されています