技術記事

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 を展開すればそこにそのまま見えます。任意のパスワードも、何かをかき乱す鍵ではなく、短い旧式ハッシュとして保存されます。ファイル名を変え、パーツを開いて、<sheetProtection> 行を取り除ける人なら、すべてを読み、すべてを編集できます

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

相互作用ポリシーとしての Delphi XLSX シート保護と、唯一の真のロックとしての AES ブック暗号化の対比図(HotXLS)
シート保護は編集を拒否しますが、値は平文のままです。パッケージを暗号化するのはワークブック暗号化だけです

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

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

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

個別のビットは、TXLSXWorksheet のインデックス付き AllowOption プロパティで読み書きします。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;

    // パスワード付きで保護します。これは保護状態とハッシュを設定するだけで、
    // オプション集合は全許可の既定値のまま残ります。
    sh.Protect('HotXLS-2026');

    // 絞り込み: 並べ替えと AutoFilter は許可し、形状変更と書式変更は禁止します。
    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つあります。Sort と AutoFilter の行は、どちらも既定値がTrue なのに明示的に書かれています。これは次の保守担当者のための説明であって、機能要件ではありません。しかも既定値は許可側なので、出力ファイルを変えるのはFalse を設定した行だけです。これはこの API の偶然ではなく、OOXML のワイヤ形式がそのまま見えているのです。次の節で見ます

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

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

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

// 上の抜粋の概念的な出力(簡潔にするため属性は省略):
// <sheetProtection sheet="1"
//   formatCells="0" formatColumns="0" formatRows="0"
//   insertRows="0" deleteRows="0"
//   password="...4-hex..."/>
// そこに「ない」ものに注目: sort、autoFilter、selectLockedCells はありません。
// それらの欠落こそが、Excel にそれらの操作が許可されたままだと伝えるのです。

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

6 つのファミリーに分類される 15 の HotXLS TXLSXSheetProtectionOption 値の図。Delphi の AllowOption インデックスプロパティと SheetProtectionOptions セットを通じて切替
15 のオプションは 6 つのファミリーに分かれ、それぞれビットごとに切り替えるか、Pascal セットとして丸ごと入れ替えられます

保護の読み戻し: ラウンドトリップの忠実性

書き込めても読み戻せない権限モデルは一方通行で、よくある症状は、読み込み・編集・保存のサイクルで権限が静かに広がってしまうことです。HotXLS はそこを塞ぎます。ParseWorksheetXml が <sheetProtection> 要素に当たると、シートを保護状態にし、存在すればパスワードハッシュを取り込み、そのうえで各操作別属性を同じ規則の逆向きで AllowOption にデコードし直します。属性が存在して "0" と等しければその操作を禁止し、属性がなければオプションは許可済みの既定値のままです

var
  wb: TXLSXWorkbook;
  sh: TXLSXWorksheet;
begin
  wb := TXLSXWorkbook.Create;
  try
    wb.Open('protection.xlsx');
    sh := wb.Sheets[1];                  // XLSX シートは 1 ベース
    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;

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

Delphi における HotXLS XLSX sheetProtection エンコードルールの図。省略された属性はアクションを許し、attr=0 は禁止。旧ハードコード行と最小限の禁止専用ライター出力の対比
許可されたアクションは属性を一切寄与せず、禁止されたアクションだけが attr=0 として現れます

実務上の注意と制限

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

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

この細かな保護モデルと XLSX 読み書きエンジンの残りの部分は、Delphi と C++Builder 向けの HotXLS Delphi Component に同梱されています。製品ページには、保護オプションの完全なリファレンスを含むワークシート API 全体が掲載されています