Bài viết kỹ thuật

Mã hóa XLSX đầu ra bằng AES trong Delphi: Những gì SaveAsEncrypted của HotXLS ghi ra

Excel cung cấp hai tính năng cùng được gọi là "mật khẩu" (password), nhưng chỉ một trong số đó thực sự là mã hóa. Mật khẩu mở tệp kích hoạt một mật mã thực sự: không có nó tệp tin hoàn toàn không thể đọc được. Trong khi đó, mật khẩu bảo vệ trang tính (worksheet) và sổ làm việc (workbook) không hề làm điều đó. Chúng chỉ thiết lập một lá cờ hiệu (flag) mà các trình soạn thảo tự giác tuân thủ, và một sổ làm việc chỉ mang lá cờ hiệu đó thực chất là một tệp nén ZIP thông thường có thể đọc được với toàn bộ dữ liệu hiển thị dưới dạng văn bản thuần (cleartext). Nếu chọn sai tính năng, bạn có thể gửi đi một bảng lương trông có vẻ đã khóa trong Excel nhưng thực tế lại đọc được bằng bất kỳ trình soạn thảo văn bản nào

Việc chứng minh điều này chỉ mất mười giây. Hãy đổi tên một tệp .xlsx được bảo vệ thành .zip, mở nó bằng bất kỳ công cụ giải nén nào, và xem tệp xl/worksheets/sheet1.xml. If các giá trị ô hiển thị ở định dạng UTF-8 thuần túy thì tệp tin đó không hề được mã hóa, bất kể Excel hiển thị bao nhiêu yêu cầu nhập mật khẩu khi ai đó cố gắng chỉnh sửa một ô. Kẽ hở này tồn tại nhiều năm trong các đội ngũ lầm tưởng bảo vệ trang tính là bảo mật thông tin, và nó thường bị phát hiện vào ngày bộ phận bảo mật chạy thử nghiệm việc đổi tên tệp này

HotXLS là một thư viện bảng tính gốc dành cho Delphi và C++Builder, và nó giữ hai tính năng này ở hai phía rõ ràng của ranh giới đó. Bảo vệ trang tính và sổ làm việc chỉ là các hạn chế chỉnh sửa được hỗ trợ bởi một thuật toán băm cũ khá yếu được giữ lại từ trước. Hàm SaveAsEncrypted tạo ra một gói dữ liệu mã hóa AES mà không một công cụ nào mở được nếu thiếu mật khẩu. Các phần dưới đây trình bày chi tiết những gì lệnh gọi này ghi ra, tính bất đối xứng mà bạn cần lưu ý khi thiết kế hệ thống (HotXLS ghi được tệp mã hóa nhưng không thể đọc ngược lại), và sự khác biệt của đường dẫn XLS cũ hơn

Tại sao bảo vệ trang tính không phải là mã hóa

Các phương thức Protect trên trang tính và ProtectWorkbook trên sổ làm việc lưu trữ một mã băm dài 4 ký tự hex của mật khẩu. Đó là thuật toán cũ mà cả OOXML và BIFF thừa hưởng từ phiên bản Excel thập niên 1990, và tài liệu định dạng chưa bao giờ tuyên bố nó có tác dụng lớn hơn việc ngăn chặn các chỉnh sửa vô tình. Gói tài liệu vẫn là một tệp nén ZIP có thể đọc bình thường: dữ liệu ô, công thức, và các chuỗi dùng chung đều nằm dưới dạng văn bản thuần XML. Thiết lập mặc định thậm chí còn tệ hơn: mọi ô bắt đầu với Locked=True, do đó việc gọi hàm Protect mà không mở khóa một vùng nhập liệu trước tiên sẽ khóa toàn bộ trang tính chống chỉnh sửa trong khi vẫn để lộ mọi giá trị dữ liệu

Không có điều nào ở trên làm cho tính năng bảo vệ trở nên vô dụng. Việc hướng dẫn người dùng nhập liệu đúng vùng và cố định bố cục để in ấn là các tác vụ thiết thực, được trình bày trong bài viết về bảo vệ trang tính và thiết lập trang. Nhưng đó là các công việc phục vụ trải nghiệm người dùng (usability). Ngay khi yêu cầu đặt ra là bảo mật thông tin (confidentiality), API duy nhất đáp ứng điều đó là SaveAsEncrypted

Những gì SaveAsEncrypted thực tế ghi ra

Triển khai thực tế tuân theo Mã hóa tiêu chuẩn ECMA-376, được quy định trong mục [MS-OFFCRYPTO] phần 2.3.4. Mật khẩu được chạy qua 50,000 vòng lặp SHA-1 để dẫn xuất ra khóa AES-128. Một khối xác thực verifier (được mã hóa bằng AES-128 ở chế độ ECB) cho phép trình đọc xác nhận mật khẩu trước khi giải mã bất kỳ thứ gì, và toàn bộ gói sổ làm việc sau đó được mã hóa bằng AES-128 ở chế độ CBC. Những gì được lưu trên đĩa hoàn toàn không phải là một tệp ZIP. Nó là một tệp Compound File của OLE chứa các luồng dữ liệu EncryptionInfo, EncryptedPackage, và DataSpaces, không có thư mục xl/ để các công cụ giải nén liệt kê, đó là lý do tại sao thử nghiệm đổi tên tệp bây giờ không hiển thị bất kỳ nội dung nào có thể đọc được. Excel 2007 và mới hơn mở tệp này chỉ với mật khẩu, và phiên bản LibreOffice hiện tại cũng đọc được Mã hóa tiêu chuẩn này

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;

Hãy xử lý biến mật khẩu với sự cẩn trọng giống như một chuỗi kết nối cơ sở dữ liệu. Lấy mật khẩu từ kho lưu trữ bảo mật (vault) hoặc dịch vụ tạo khóa bí mật vào thời điểm cuối cùng, không bao giờ ghi nhật ký (log) mật khẩu và không bao giờ viết nó vào chính sổ làm việc. Việc kiểm tra mã trả về không phải là một thủ tục tùy chọn. Một lượt lưu mã hóa bị thất bại giữa chừng bắt buộc phải hủy bỏ quá trình gửi tệp, bởi vì giải pháp dự phòng duy nhất mà mã nguồn có thể đưa ra là một bản sao không mã hóa, và bản sao đó chính là sự cố bảo mật mà tính năng này tồn tại để ngăn chặn

Có một bài kiểm thử tự động với chi phí cực thấp: gọi hàm CanReadEncrypted trên tệp tin bạn vừa ghi. Nó chỉ trả về true khi kết quả thực sự là một thùng chứa mã hóa, do đó việc kiểm tra khẳng định (assert) nó sau mỗi lượt lưu mã hóa sẽ giúp phát hiện lỗi nghiêm trọng nhất — đường dẫn mã nguồn âm thầm chuyển về một lệnh SaveAs thông thường — ngay tại thời điểm nó xảy ra chứ không phải vài tuần sau đó trong hộp thư đến của khách hàng. Bài kiểm thử cuối cùng vẫn là việc mở thủ công tệp tin trong Excel bằng mật khẩu thực tế trong quá trình thử nghiệm phát hành

Chỉ ghi theo thiết kế: xử lý ngoại lệ EXlsxEncryptionNotImplemented

Đây là tính bất đối xứng quyết định cấu trúc đường ống xử lý của bạn: HotXLS thực hiện mã hóa khi lưu nhưng không giải mã khi mở. Hàm OpenEncrypted sẽ kích hoạt ngoại lệ EXlsxEncryptionNotImplemented khi được trỏ vào một gói tài liệu đã mã hóa thực tế; trên một sổ làm việc thông thường nó chỉ đơn giản chuyển tiếp sang lệnh gọi Open bình thường. Hàm kiểm tra đi kèm CanReadEncrypted giúp phát hiện thùng chứa mã hóa OLE với chi phí thấp, nhờ đó mã nguồn tiếp nhận có thể điều hướng các tệp đó mà không làm kích hoạt ngoại lệ:

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.CanReadEncrypted(FileName) then
    begin
      // Encrypted container: HotXLS cannot decrypt it.
      Writeln(FileName + ': needs manual decryption in Excel first');
      Exit;
    end;
    try
      Book.OpenEncrypted(FileName, '');   // plain files fall through to 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;

Tính bất đối xứng đó dẫn đến một kết luận kiến trúc rõ ràng: chỉ mã hóa ở bước cuối cùng trước khi gửi đi. Hãy giữ bản gốc văn bản thuần bên trong ranh giới tin cậy của bạn (trong cơ sở dữ liệu, kho lưu trữ tài liệu hoặc thư mục chia sẻ được kiểm soát quyền truy cập) và tạo ra bản sao mã hóa làm bước cuối cùng trước khi tệp tin rời khỏi hệ thống. Một quy trình chỉ lưu trữ tệp đầu ra đã mã hóa sẽ tự khóa quyền truy cập dữ liệu của chính mình, vì không có bước nào tiếp theo của cùng hệ thống có thể mở lại các tệp đó. Khi một quy trình HotXLS ở hạ nguồn cần sổ làm việc đó một lần nữa, hãy cung cấp cho nó bản gốc văn bản thuần chứ không bao giờ dùng tệp đã mã hóa gửi đi

Mã hóa tiêu chuẩn AES-128 và tiêu chuẩn tuân thủ AES-256

Cơ chế mã hóa tệp tin của Office có hai thế hệ. Mã hóa tiêu chuẩn (Standard Encryption) là phiên bản HotXLS ghi ra, sử dụng thuật toán AES-128 với dẫn xuất khóa SHA-1. Mã hóa Agile (Agile Encryption) ra đời sau đó, nâng cấp lên AES-256 với SHA-512 và một thùng chứa khóa khác được mô tả bằng XML. Cả hai đều mở bình thường trong Excel, và AES-128 vẫn an toàn về mặt tính toán để bảo vệ một tệp tin trong quá trình gửi tới khách hàng

Sự khác biệt không còn là lý thuyết suông vào ngày một bảng câu hỏi bảo mật yêu cầu "mã hóa AES-256 cho các tệp tĩnh lưu trữ (at rest)". Mã hóa tiêu chuẩn không đáp ứng yêu cầu đó, bất kể mật khẩu mạnh đến mức nào, và không có tham số nào của SaveAsEncrypted có thể thay đổi thuật toán mà nó xuất ra. Vì vậy, hãy mô tả chính xác cấu hình mã hóa trong tài liệu bảo mật của bạn: AES-128, Mã hóa tiêu chuẩn ECMA-376, dẫn xuất khóa SHA-1 với 50,000 vòng lặp. Một tuyên bố thực tế vượt qua vòng kiểm duyệt luôn có giá trị hơn một kỳ vọng lạc quan nhưng lại bị đổ bể khi kiểm toán

Đường dẫn XLS cũ: ghi RC4, đọc ngược lại RC4 và XOR

Giao diện BIFF có đặc tính ngược lại. Cơ chế mã hóa của nó cũ hơn và yếu hơn, nhưng chu trình đọc-ghi là hoàn chỉnh: những gì nó ghi ra, nó hoàn toàn có thể đọc ngược lại. Thiết lập EncryptionPassword trước khi gọi SaveAs tạo ra một tệp .xls mã hóa RC4 thông qua cơ chế FilePass của BIFF, và lệnh gọi Open với tham số mật khẩu đọc được cả ba cơ chế cũ: RC4, RC4 CryptoAPI, và kiểu làm mờ XOR cổ xưa:

var
  Writer, Reader: IXLSWorkbook;   // interface refs: no manual 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);  // Entries are 1-based
end;

RC4 là công nghệ mật mã đã lỗi thời và không bao giờ nên được dùng để bảo vệ các dữ liệu quan trọng ngày nay; giá trị duy nhất còn lại của nó là khả năng tương thích với các hệ thống vẫn trao đổi tệp .xls. Phía đọc của nó rất hữu ích trong các tác vụ di chuyển dữ liệu. Một tệp tin cũ được bảo vệ bằng mật khẩu sẽ mở bằng lệnh Open(FileName, Password), chuyển đổi sang mô hình OOXML và bảo mật lại qua đường dẫn AES, một tiến trình nâng cấp một chiều hoạt động độc lập mà không cần Excel trong chu trình. Đối với các lượt gửi tệp mã hóa số lượng lớn, các lưu ý về hiệu năng ghi tệp trong bài viết về ghi dữ liệu dạng luồng cho các tác vụ hàng loạt trên máy chủ sẽ áp dụng cho giai đoạn xây dựng nội dung diễn ra trước khi mã hóa

Mã hóa và bảo vệ không loại trừ lẫn nhau

Một điểm nữa cần được làm rõ, vì nó thường phát sinh ngay khi ai đó hiểu nhầm cảnh báo ở đầu trang này thành "tính năng bảo vệ là vô dụng". Thực tế không phải vậy. Mã hóa và bảo vệ giải quyết các bài toán khác nhau, và chúng bổ trợ cho nhau một cách hoàn hảo. Mã hóa quyết định ai có quyền mở tệp; bảo vệ quyết định những gì người đọc (đã vào được bên trong) có thể chỉnh sửa. Một bảng lương hoàn toàn có thể áp dụng cả hai: mã hóa gói tài liệu để chỉ người có mật khẩu mới xem được, đồng thời khóa các ô công thức để người nhận có thể lọc và sắp xếp nhưng không thể sửa đổi các công thức tính toán. Sai lầm không phải là việc thêm tính năng bảo vệ. Sai lầm là để sự hiện diện của nó thay thế cho vai trò của mã hóa khi yêu cầu thực tế là bảo mật thông tin

Phía quản lý mật khẩu không có mạng lưới an toàn dự phòng nào, và đó là thiết kế có chủ ý. Tiến trình dẫn xuất khóa 50,000 vòng lặp tồn tại để làm cho việc đoán mật khẩu trở nên cực kỳ đắt đỏ, và không có gì bên trong tệp tin lưu giữ khóa bí mật đó. Mất mật khẩu đồng nghĩa với mất dữ liệu. Hãy tạo, chuyển giao và lưu trữ các mật khẩu này với cùng tính kỷ luật mà bạn áp dụng cho thông tin đăng nhập cơ sở dữ liệu, và tính năng mã hóa sẽ thực hiện đúng vai trò của nó

Mã hóa tệp tin thực sự chỉ là một lệnh gọi trong HotXLS. Tính kỷ luật nằm ở mọi thứ xung quanh lệnh gọi đó: việc quản lý mật khẩu, ranh giới chỉ ghi giữ cho HotXLS không tự mở lại kết quả đầu ra của nó, và một cấu hình thuật toán mã hóa mà bạn có thể tự tin giải trình khi kiểm toán. Hàm SaveAsEncrypted và chu trình đọc-ghi cũ đi kèm trong HotXLS Component, chạy độc lập trong các tiến trình Delphi và C++Builder mà không yêu cầu bất kỳ tiến trình tự động hóa Excel nào