技術記事

DelphiでのXLSXシート保護: 15個の許可オプション

完成したブックを同僚に渡して、書き換えではなくフィルターだけ使ってほしいと頼むことがあります。そこでシートを保護します。以前のHotXLSでは、その操作はファイルに1つだけを書き込みました: <sheetProtection sheet="1" objects="1" scenarios="1"/>, それだけを毎回固定で書いていました。シートはロックされ、パスワードハッシュが付く一方で、ユーザーは何もできませんでした。残しておきたかった並べ替えやフィルターさえ使えません。Excelの「シートの保護」ダイアログに15個のチェックボックスがあるのはまさにそのためで、当時のエンジンはそれらを1つも表現できませんでした。その差を埋めるのが v2.91.0 の保護モデルです

HotXLS は、Excel をインストールしていなくても XLS と XLSX を読み書きできる、Delphi と C++Builder 向けのネイティブ VCL スプレッドシートコンポーネントです。この記事では、ワークシート保護のXLSX側、つまり新しい TXLSXSheetProtectionOption 列挙型、各権限を切り替える AllowOption プロパティ、そして <sheetProtection> 要素を手で書くときに誰もがつまずく OOXML の1つのエンコード規則を扱います

ワークシート保護は実際には何を守るのか

まず境界をはっきりさせます。これが、ここまでの説明をどこまで信頼すべきかを決めるからです。OOXML スプレッドシート形式(ECMA-376)におけるワークシート保護は、暗号化ではなく操作ポリシーです。保護中のシートでどの編集を拒否するかを、準拠アプリケーションに伝えます。セル値はまだ xl/worksheets/sheetN.xml に平文のままあり、.xlsx を展開すればそこに見えます。任意のパスワードも、何かを暗号化する鍵ではなく、短い旧式ハッシュとして保存されます。ファイル名を変え、パーツを開いて、 行を取り除ける人なら、すべて読み、すべて編集できますxl/worksheets/sheetN.xml の中にある。.xlsx を展開すれば.xlsx を開けば、そこにそのまま入っています。任意のパスワードは短い旧式ハッシュとして保存されており、何かをかき乱す鍵ではありません。ファイル名を変え、パーツを開いて、<sheetProtection> 行を取り除ける人なら、すべて読み、すべて編集できます

つまり保護が答えるのは「同僚がうっかり数式を壊さないようにする」ことであって、「意図を持った相手からこのデータを秘匿する」ことではありません。これは別の問題であり、使う道具も別です。機密性が必要なら、AES保護のXLSX出力 で説明しているワークブックレベルの暗号化を使うべきです。シート保護とワークブック暗号化はきれいに組み合わせられますが、本当の鍵になるのは後者だけです。この区別を保てば、あとは配線の話だけです

15個のオプションと AllowOption プロパティ

各ワークシートは今、保護中にユーザーがまだ何をできるかを示す TXLSXSheetProtectionOption の値を保持します。各メンバーは OOXML 属性と Excel のダイアログのチェックボックスに1対1で対応します:

  • xlsxSpoEditObjects, xlsxSpoEditScenariosxlsxSpoEditObjects, xlsxSpoEditScenarios、図形オブジェクトとシナリオを編集する
  • xlsxSpoFormatCells, xlsxSpoFormatColumns, xlsxSpoFormatRowsxlsxSpoFormatCells, xlsxSpoFormatColumns, xlsxSpoFormatRows、セル、列、行の書式を変更する
  • xlsxSpoInsertColumns, xlsxSpoInsertRows, xlsxSpoInsertHyperlinksxlsxSpoInsertColumns, xlsxSpoInsertRows, xlsxSpoInsertHyperlinks、列、行、リンクを挿入する
  • xlsxSpoDeleteColumns, xlsxSpoDeleteRowsxlsxSpoDeleteColumns, xlsxSpoDeleteRows、列と行を削除する
  • xlsxSpoSelectLockedCells, xlsxSpoSelectUnlockedCellsxlsxSpoSelectLockedCells, xlsxSpoSelectUnlockedCells、ロック済みセルまたは未ロックセルへ選択を移動する
  • xlsxSpoSort, xlsxSpoAutoFilter, xlsxSpoPivotTablesxlsxSpoSort, xlsxSpoAutoFilter, xlsxSpoPivotTables、範囲の並べ替え、AutoFilter のドロップダウン使用、PivotTable の操作

個別のビットは、インデックス付きの AllowOption プロパティで読み書きします。TXLSXWorksheet AllowOption[Opt] = True なら、その操作は許可されます。False を設定すると禁止されます。集合全体は SheetProtectionOptions からもまとめて参照できます。これは TXLSXSheetProtectionOptions、つまり単なる Pascal の set of なので、保存、復元、あるいは丸ごとの置き換えができますAllowOption[Opt] = True はその操作を許可します。False を設定すると禁止されます。集合全体は SheetProtectionOptions, TXLSXSheetProtectionOptions(素の Pascal の set of)としても参照できるので、保存、復元、あるいは丸ごとの置き換えができます

既定値には意図があります。新規作成したワークシートでは、すべてのオプションが許可された状態で始まります。コンストラクタは SheetProtectionOptions に全範囲を入れ、[Low(TXLSXSheetProtectionOption)..High(TXLSXSheetProtectionOption)] で初期化します。そこから、禁止したい操作だけを除外していきます。権限集合を一から作るのではありません。この選択が、後で出てくるライターのエンコード規則を Excel の挙動にきれいに合わせます

シートを保護しつつ、並べ替えとフィルターは開いたままにする

よくあるケースを最初から最後まで示します。完成したレポートを保護してレイアウトを崩せないようにしつつ、読み手には並べ替えとフィルターを許します。Protect とオプションは独立しています。Protect はシートを保護状態にして任意のパスワードハッシュを保存しますが、オプション集合には触れません。AllowOption を別に設定し、シートを保護して保存した時点で切り替えが有効になります

var
  wb: TXLSXWorkbook;
  sh: TXLSXWorksheet;
begin
  wb := TXLSXWorkbook.Create;
  try
    sh := wb.Sheets.Add('Protected');
    sh.Cells[1, 1].Value := 'Region'; sh.Cells[1, 2].Value := 'Units';
    sh.Cells[2, 1].Value := 'North';  sh.Cells[2, 2].Value := 120;
    sh.Cells[3, 1].Value := 'South';  sh.Cells[3, 2].Value := 98;

    // Protect with a password. This only sets the protected state + hash;
    // the option set is left at its all-permitted default.
    sh.Protect('HotXLS-2026');

    // Narrow: keep sort + AutoFilter, forbid reshaping and reformatting.
    sh.AllowOption[xlsxSpoSort]          := True;
    sh.AllowOption[xlsxSpoAutoFilter]    := True;
    sh.AllowOption[xlsxSpoFormatCells]   := False;
    sh.AllowOption[xlsxSpoFormatColumns] := False;
    sh.AllowOption[xlsxSpoFormatRows]    := False;
    sh.AllowOption[xlsxSpoInsertRows]    := False;
    sh.AllowOption[xlsxSpoDeleteRows]    := False;

    if wb.SaveAs('protection.xlsx') <> 1 then
      Writeln('SaveAs failed');
  finally
    wb.Free;
  end;
end;

この抜粋から読み取れることは2つあります。SortAutoFilter の行は、どちらも既定値がTrue なのに明示的に書かれています。これは次の保守担当者のための説明であって、機能要件ではありません。しかも既定値は許可側なので、出力ファイルを変えるのはFalse を設定した行だけです。これはこの API の偶然ではなく、OOXML のワイヤ形式がそのまま見えているのです。次の節で見ます

エンコード規則: 省略は許可、attr=0 は禁止

これは機能全体で唯一の直感に反する事実で、手書きの<sheetProtection> がたいてい失敗する箇所です。OOXML では、各操作別属性は禁止フラグであり、その欠落が許可を意味します。属性がなければ、その操作は許可されています。属性が"0"と書かれている場合は、シート保護中はその操作が禁止されます。整形式のファイルでformatCells="1"と書いて「書式設定を許可」を表すことはありません。単に属性を書かないだけです。(属性がない場合の既定値は OOXML の真偽値既定である true であり、これらの属性名は「true が対応する編集の許可」を意味するように付けられています)

HotXLS のライターはそれをそのまま再現します。sheet="1" 保護を有効にしたあと、オプション集合を走査して、attr="0" 設定したオプションにだけFalseを書き込みます。許可された操作は出力に何も足しません。したがって、前節のワークブックは、禁止した操作とパスワードハッシュだけを持つ次のような形にシリアライズされます:

// Conceptual output for the snippet above (attributes elided for brevity):
// <sheetProtection sheet="1"
//   formatCells="0" formatColumns="0" formatRows="0"
//   insertRows="0" deleteRows="0"
//   password="...4-hex..."/>
// Note what is NOT there: no sort, no autoFilter, no selectLockedCells.
// Their absence is exactly what tells Excel those actions stay allowed.

古い固定文字列から来ていて、すべての属性が明示されることを期待していると、この出力は薄すぎる、ほとんど間違って見えるかもしれません。ですが正しいのです。sort="1"autoFilter="1" を列挙したファイルでも、準拠した読み手には同じ意味になります。しかし Excel 自身は最小限の禁止のみの形式を書き出し、これに合わせれば差分は小さく、往復保存もおとなしいままです。objectsscenarios 属性も同じ規則に従います。これらは既定で許可なので、禁止したときだけ "0" として現れます。これは、以前は無条件に出力されていた古い objects="1" scenarios="1" の逆です

書き込めても読み戻せない権限モデルは一方通行で、よくある症状は、読み込み・編集・保存のサイクルで権限が静かに広がってしまうことです。HotXLS はそこを塞ぎます

ParseWorksheetXml 要素に当たると、シートを保護状態にし、存在すればパスワードハッシュを取り込み、そのうえで各操作別属性を <sheetProtection> 同じ規則を逆向きに使って、属性が AllowOption 同じ規則を逆向きに使って、属性が "0" ならその操作を禁止し、属性がなければオプションは許可済みの既定値のままです

var
  wb: TXLSXWorkbook;
  sh: TXLSXWorksheet;
begin
  wb := TXLSXWorkbook.Create;
  try
    wb.LoadFromFile('protection.xlsx');
    sh := wb.Sheets[1];                  // XLSX sheets are 1-based
    if sh.IsProtected then
    begin
      Writeln('Protected; password hash present: ',
        sh.SheetProtectHash <> '');
      Writeln('Sort allowed:       ', sh.AllowOption[xlsxSpoSort]);
      Writeln('AutoFilter allowed: ', sh.AllowOption[xlsxSpoAutoFilter]);
      Writeln('FormatCells allowed:', sh.AllowOption[xlsxSpoFormatCells]);
    end;
  finally
    wb.Free;
  end;
end;

ライターが生成したファイルを読み込むと、SortAutoFilterTrue, FormatCells として戻りますFalse、保存した集合はそのまま保たれます。この対称性こそが要点です。保護された一部許可のシートで1つのセルを編集して再保存しても、触っていない14個の権限は古い全か無かの既定に崩れずに残ります

実務上の注意と制限

この機能をレポートの処理フローに組み込む前に、知っておくとよい点がいくつかあります:

  • パスワードは設計上、弱いものです。 XLSX ワークシート保護は、16ビットの旧式ハッシュを保存します(Excel が何十年も使ってきたものと同じです)。互換性のために残されています。偶発的な編集は防ぎますが、攻撃者は止められません。秘密保持手段とは考えないでください。本当に守るなら、ワークブックを暗号化してください
  • オプションを保護前に設定しても問題ありません。 AllowOption は、シートが現在保護されているかどうかに関係なく代入できます。トグルは、Protect が有効になったときに保護で何を許可するかを示すだけです。UnProtect は保護状態とハッシュをクリアしますが、オプション集合は次回まで残します
  • ロック済みセルの意味は引き続き有効です。 保護がブロックするのは、Locked 属性が設定されたセル(ワークブックの既定値です)への編集だけです。入力領域を編集可能のままにするのはセルスタイルの役割であって、保護オプションではありません。2つの層は Excel と同じように組み合わさります
  • これは XLSX エンジンです。 オプションモデルは XLS エンジンの古い Allow* プロパティに対応していますが、ここでの列挙型とプロパティ名(xlsxSpo*, AllowOption) は TXLSXWorksheetlxHandleX に属します。 同じシートで印刷レイアウトも扱うなら、保護とページ設定の解説 では、これらの設定が印刷範囲やヘッダーとどう並ぶかを説明しています。また、データ検証、AutoFilter、テーブルxlsxSpoAutoFilter は、ロックされたレポートで

を開いたままにする運用と相性がいいですXLSX の読み書きエンジン全体と、この細かな保護モデルは HotXLS Component