技術記事

HotXLS Delphi Component: Delphi での XLSX AES-protected output

Excel には「パスワード」と呼ばれるものが2つ存在し、暗号化と呼べるのはそのうちの片方だけです。開くパスワードは本物の暗号を鍵として使い、それがなければファイルはまったく読めません。ワークシート保護とワークブック保護のパスワードはそのような働きを一切しません。これらは協調的なエディタが尊重することに同意するフラグを立てるだけであり、そのフラグしか持たないワークブックは、データが平文のまま入った、ただの読み取り可能な zip です。誤ったほうを選んでしまうと、Excel 上では施錠されているように見えて、任意のテキストエディタでは読める給与データを出荷することになります

この証明は10秒あれば済みます。保護された .xlsx.zip にリネームし、任意のアーカイブツールで開いて xl/worksheets/sheet1.xml を見てください。セルの値がそのまま平文の UTF-8 で入っているなら、そのファイルは暗号化されていません。誰かがセルを編集しようとしたときに Excel がどれだけパスワードプロンプトを出そうと関係ありません。このギャップは、シート保護を機密性だと思い込んでいるチームの中で何年も生き延び、たいていはセキュリティレビューがまさにこのリネームを実行した日に表面化します

HotXLS はネイティブの Delphi/C++Builder スプレッドシートライブラリであり、この2つの機能をその境界線の両側にはっきりと分けて扱います。ワークシート保護とワークブック保護は、意図的に弱いレガシーハッシュに支えられた編集制限です。SaveAsEncrypted は、パスワードなしでは開けない AES 暗号化パッケージを生成します。以下のセクションでは、この呼び出しが実際に書き込む内容、設計上考慮しなければならない非対称性(HotXLS は暗号化ファイルを書き込めますが、それを読み戻すことはできません)、そして旧来の XLS 経路がどう異なるかを扱います

弱いハッシュを格納しセルデータをプレーン zip で読めるまま残す Delphi XLSX シート保護と、AES-128 鍵を導出し OLE 暗号化コンテナを書く HotXLS SaveAsEncrypted の対比図
シート保護は弱いハッシュを保存し、パッケージを読める zip のままにします。SaveAsEncrypted は AES-128 鍵を導出し、アーカイブツールに一覧できない OLE コンテナーを書きます

シート保護が暗号化ではない理由

シートの Protect メソッドとワークブックの ProtectWorkbook は、パスワードの 4 桁の16進ハッシュを保存します。これは OOXML と BIFF がともに1990年代の Excel から受け継いだレガシーアルゴリズムであり、フォーマットのドキュメントも、これが偶発的な編集を止める以上のことをするとは一切主張していません。パッケージは通常どおり読み取り可能な zip のままであり、セルデータ、数式、共有文字列はすべて平文の XML です。既定の挙動はこれをさらに悪化させます。すべてのセルは Locked=True から始まるため、入力範囲のロックを先に解除せずに Protect を呼び出すと、すべての値を平文のまま見せたままシート全体を編集不可に凍結してしまいます

とはいえ、保護がまったく無意味というわけではありません。ユーザーを編集可能な範囲に誘導したり、印刷用にレイアウトを固定したりするのは実際に役立つ仕事であり、ワークシート保護とページ設定に関する記事で扱っています。しかし、それらはあくまで使い勝手の仕事です。要件が機密性である瞬間、それに応えられる API は SaveAsEncrypted だけです

SaveAsEncrypted が実際に書き込むもの

この実装は [MS-OFFCRYPTO] セクション 2.3.4 で規定される ECMA-376 Standard Encryption に従います。パスワードは SHA-1 の 50,000 回の反復にかけられ、AES-128 鍵を導出します。ECB モードの AES-128 で暗号化された検証ブロックにより、消費者は何かを復号する前にパスワードを確認でき、ワークブックパッケージ全体はその後 CBC モードの AES-128 で暗号化されます。ディスク上に残るのはそもそも zip ではありません。EncryptionInfoEncryptedPackage、DataSpaces の各ストリームを保持する OLE 複合ファイルであり、アーカイブツールが一覧表示できる xl/ ディレクトリは存在しません。だからこそ、先ほどのリネームテストではもう何も読み取れるものが出てこないのです。Excel 2007 以降はパスワードだけでこれを開けますし、現行の LibreOffice も Standard Encryption を読み取れます

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  rc: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Payroll');
    Sheet.Cells[1, 1].Value := 'Employee';
    Sheet.Cells[1, 2].Value := 'Net pay';
    Sheet.Cells[2, 1].Value := 'A. Garcia';
    Sheet.Cells[2, 2].Value := 4815.16;

    rc := Book.SaveAsEncrypted('payroll-2026-06.xlsx', PasswordFromVault);
    if rc <> 1 then
      raise Exception.CreateFmt('Encrypted save failed (rc=%d)', [rc]);
  finally
    Book.Free;
  end;
end;

パスワード変数は接続文字列と同じ注意を払って扱ってください。ボールトや生成済みシークレットサービスから最後の瞬間に取得し、決してログに残さず、ワークブック自体にも決して書き込まないでください。戻り値のチェックは省略できる儀式ではありません。暗号化保存が途中で失敗した場合、配信を中止しなければなりません。呼び出し側コードが提供できる唯一のフォールバックは非暗号化コピーであり、そのコピーこそがこの機能が防ごうとしているまさにそのインシデントだからです

ほとんどコストのかからない、機械的にチェック可能な受け入れテストもあります。書き込んだばかりのファイルに対して CanReadEncrypted を呼び出してください。これは出力が本当に暗号化コンテナである場合にのみ true を返すため、暗号化保存のたびにこれをアサートすれば、最も重要な回帰、つまり静かに通常の SaveAs にフォールバックしてしまったコードパスを、何週間も後に顧客の受信箱で発覚するのではなく、その場で捕捉できます。最終判断は、リリーステスト時に実際のパスワードで Excel から手動で開くことに委ねられます

設計として書き込み専用: EXlsxEncryptionNotImplemented の扱い

ここに、パイプラインのアーキテクチャを形作るべき非対称性があります。HotXLS は保存時に暗号化しますが、開くときに復号はしません。OpenEncrypted は、実際に暗号化されたパッケージを指定すると EXlsxEncryptionNotImplemented を送出します。通常のワークブックに対しては、単に通常の Open へフォールスルーします。付随するプローブ CanReadEncrypted は OLE 暗号化コンテナを安価に検出できるため、取り込みコードは例外を発生させることなくそうしたファイルをルーティングできます

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.CanReadEncrypted(FileName) then
    begin
      // 暗号化コンテナ: HotXLS はこれを復号できない
      Writeln(FileName + ': needs manual decryption in Excel first');
      Exit;
    end;
    try
      Book.OpenEncrypted(FileName, '');   // 通常のファイルは Open にフォールスルーする
      Writeln(FileName + ': opened, ' + IntToStr(Book.Sheets.Count) + ' sheet(s)');
    except
      on EXlsxEncryptionNotImplemented do
        Writeln(FileName + ': encrypted - routed to manual queue');
    end;
  finally
    Book.Free;
  end;
end;

その非対称性には、はっきりとしたアーキテクチャ上の読み方が1つあります。配信の末端で、最後の最後に暗号化するということです。平文のマスターは、データベース、ドキュメントストア、アクセス制御された共有領域など、信頼境界の内側に保持し、ファイルがシステムを離れる直前の最終ステップとして暗号化コピーを生成してください。暗号化済みの出力だけをアーカイブするパイプラインは、自分自身のデータから締め出されてしまいます。同じシステムのどの後段のステージも、それらのファイルを再び開くことができないからです。下流の HotXLS プロセスがワークブックを再び必要とするときは、配信用の成果物ではなく、常に平文のマスターを渡してください

Delphi HotXLS SaveAsEncrypted 呼び出しのパイプライン図。保管庫パスワードを 50,000 回の SHA-1 ラウンドで回して AES-128 鍵へ、ECB ベリファイアブロックと CBC パッケージ暗号化、CanReadEncrypted で確認される OLE 複合ファイルを生成
SaveAsEncrypted の 1 呼び出しが、金庫のパスワードを AES-128 鍵と OLE 複合ファイルへ変えます。CanReadEncrypted が、機械的に検証可能な受け入れゲートを保存に与えます

AES-128 Standard Encryption と AES-256 コンプライアンスの境界線

Office ファイルの暗号化には2つの世代があります。HotXLS が書き込む Standard Encryption は、SHA-1 鍵導出を伴う AES-128 を使用します。後発の Agile Encryption は AES-256 と SHA-512、そして XML で記述された別の鍵コンテナへと移行しました。どちらも Excel では透過的に開けますし、AES-128 は顧客への転送中のファイルを保護するには今なお計算量的に十分健全です

Delphi サービス向けアーキテクチャ図。平文ブックマスターは信頼境界内に留まり、HotXLS SaveAsEncrypted が配送エッジの最終ステップとして走る。暗号化コピーのみをアーカイブしないよう警告
信頼境界の内側に置いた平文マスターから、配信の末端で、最後に暗号化します。暗号化コピーだけを保管すると、後のすべての段階が自分のデータから締め出されます

この違いが机上の議論ではなくなるのは、セキュリティ質問票が「保存時のファイルの AES-256 暗号化」を求めてきた日です。Standard Encryption は、パスワードがどれほど強力であっても、その基準を満たしません。SaveAsEncrypted のどのパラメータも、出力するアルゴリズムを変えることはできません。したがって、セキュリティ文書にはそのプロファイルを正確に記載してください。AES-128、ECMA-376 Standard Encryption、50,000 回反復の SHA-1 鍵導出です。レビューに耐える主張は、監査で崩れる楽観的な主張よりもはるかに価値があります

レガシー XLS 経路: RC4 の退出、RC4 と XOR の復帰

BIFF のファサードはこれとは正反対の形をしています。その暗号化はより古く弱いものですが、往復は完全です。書き込んだものは読み戻すこともできます。SaveAs の前に EncryptionPassword を設定すると、BIFF の FilePass 機構を通じて RC4 で暗号化された .xls が生成され、パスワードパラメータを渡した Open は3つのレガシー方式すべて、RC4、RC4 CryptoAPI、そして古代の XOR 難読化を読み取ります

var
  Writer, Reader: IXLSWorkbook;   // インターフェース参照: 手動での Free は不要
begin
  Writer := TXLSWorkbook.Create;
  Writer.Sheets.Add.Cells.Item[1, 1].Value := 'Confidential';
  Writer.EncryptionPassword := 'S3cret!';
  Writer.SaveAs('confidential.xls');

  Reader := TXLSWorkbook.Create;
  if Reader.Open('confidential.xls', 'S3cret!') > 0 then
    Writeln(Reader.Sheets[1].Cells.Item[1, 1].Value);  // エントリは 1 始まり
end;

RC4 は時代遅れの暗号方式であり、今日重要なデータを守るために使うべきではありません。残された唯一の価値は、いまだに .xls をやり取りするシステムとの相互運用性です。とはいえ、読み取り側はマイグレーション作業においてその存在価値を発揮します。パスワードで保護されたレガシーファイルは Open(FileName, Password) で開き、OOXML モデルへブリッジし、AES 経路で再度保護をかけ直せます。これは、ループのどこにも Excel を挟まずに実行できる一方向のアップグレードです。大量の暗号化配信については、サーバーバッチジョブ向けストリーミング書き込みに関する記事にある保存側スループットの注意点が、暗号化前のコンテンツ構築フェーズに適用されます

暗号化と保護はライバルではない

もう1点、決着をつけておく価値のある話があります。というのも、このページ冒頭の警告を「保護には価値がない」と読んでしまう人がすぐに出てくるからです。そうではありません。暗号化と保護は異なる問いに答えるものであり、きれいに積み重なります。暗号化はファイルを開けるのが誰かを決め、保護はすでに中に入っている読み手が何を変更できるかを決めます。給与データの配信は、両方を合理的に行えます。パスワードの保持者だけが見られるようパッケージを暗号化し、その上で数式セルをロックして、受信者がフィルタや並べ替えはできても計算を静かに書き換えられないようにするのです。誤りは決して保護を追加することではありません。誤りは、要件が機密性であったにもかかわらず、保護の存在を暗号化の代わりとして扱ってしまうことです

保管の側にはセーフティネットが一切なく、それは意図的な設計です。50,000回の反復による鍵導出は、推測にコストをかけさせるために存在し、ファイルの内部には秘密をエスクローする仕組みは何もありません。パスワードを失えばデータも失われます。これらのパスワードは、データベースの認証情報と同じ規律で生成、配信、保管してください。そうすれば暗号化はその役目を果たし続けます

実際のファイル暗号化は、HotXLS では1回の呼び出しで済みます。規律が宿るのは、その呼び出しを取り巻くすべてのものです。パスワードの保管、HotXLS が自分自身の出力を再度開けないようにする書き込み専用の境界、そして監査で弁護できるアルゴリズムの主張です。SaveAsEncrypted とレガシーの往復は、Delphi と C++Builder のプロセス内でネイティブに動作し、経路のどこにも Excel 自動化を挟まない HotXLS Delphi Component に同梱されています