บทความเทคนิค

การเข้ารหัสเอาต์พุต XLSX ด้วย AES ใน Delphi: ข้อมูลที่เขียนโดย HotXLS SaveAsEncrypted

Excel เปิดให้ใช้สองสิ่งที่ต่างก็ถูกเรียกว่า "password" และมีแค่อย่างเดียวเท่านั้นที่เป็นการเข้ารหัสจริง ๆ open password นั้นเป็นกุญแจของ cipher จริง ๆ: ถ้าไม่มีมัน ไฟล์จะอ่านไม่ได้เลย ส่วน password ของ worksheet และ workbook protection ไม่ได้ทำแบบนั้นเลยแม้แต่น้อย มันแค่ตั้ง flag ที่โปรแกรมแก้ไขที่ให้ความร่วมมือตกลงจะเคารพเท่านั้น และ workbook ที่พกแค่ flag นั้นก็คือ zip ที่อ่านได้ธรรมดา ๆ โดยมีข้อมูลนั่งอยู่เป็น cleartext เลือกผิดตัว แล้วคุณก็จะส่งข้อมูลเงินเดือนที่ดูเหมือนล็อกไว้ใน Excel แต่อ่านได้ในโปรแกรมแก้ไขข้อความตัวไหนก็ได้

พิสูจน์ใช้เวลาแค่สิบวินาที เปลี่ยนชื่อไฟล์ .xlsx ที่ protect ไว้เป็น .zip เปิดด้วยเครื่องมือ archive ตัวไหนก็ได้ แล้วดูที่ xl/worksheets/sheet1.xml ถ้าค่าเซลล์อยู่ตรงนั้นเป็น UTF-8 ธรรมดา ไฟล์ก็ไม่ได้ถูกเข้ารหัสเลย ไม่ว่า Excel จะขึ้น prompt ขอ password กี่ครั้งเวลามีใครพยายามแก้เซลล์ก็ตาม ช่องว่างนี้อยู่รอดมาหลายปีภายในทีมที่สมมติว่า sheet protection คือความลับ และมันมักโผล่ขึ้นมาให้เห็นในวันที่การตรวจสอบความปลอดภัยทำการเปลี่ยนชื่อแบบนี้เป๊ะ ๆ

HotXLS คือไลบรารีสเปรดชีตแบบ native สำหรับ Delphi และ C++Builder และมันเก็บสองฟีเจอร์นี้ไว้คนละฝั่งของเส้นแบ่งนั้นชัดเจน worksheet และ workbook protection คือข้อจำกัดการแก้ไขที่หนุนหลังด้วย legacy hash ที่จงใจทำให้อ่อนแอ SaveAsEncrypted สร้างแพ็กเกจที่เข้ารหัสด้วย AES ซึ่งไม่มีอะไรนอกจาก password เท่านั้นที่จะเปิดมันได้ ส่วนด้านล่างนี้ครอบคลุมว่าการเรียกนั้นเขียนอะไรบ้าง ความไม่สมมาตรที่คุณต้องออกแบบรองรับ (HotXLS เขียนไฟล์ที่เข้ารหัสได้ แต่อ่านกลับไม่ได้) และเส้นทาง XLS แบบเก่าต่างออกไปอย่างไร

แผนภาพเปรียบเทียบการป้องกันชีต XLSX ของ Delphi ที่เก็บแฮชอ่อนและทิ้งข้อมูลเซลล์ให้อ่านได้ใน zip ธรรมดา กับ SaveAsEncrypted ของ HotXLS ที่ derive คีย์ AES-128 แล้วเขียนคอนเทนเนอร์เข้ารหัสแบบ OLE
การป้องกันชีตเก็บแฮชอ่อนแอและปล่อยแพ็กเกจเป็น zip ที่อ่านได้ SaveAsEncrypted สืบทอดคีย์ AES-128 และเขียนคอนเทนเนอร์ OLE ที่เครื่องมือจัดการอาร์ไคฟ์ไม่มีตัวใดลิสต์ได้

เหตุใด sheet protection จึงไม่ใช่การเข้ารหัส

เมธอด Protect บน sheet และ ProtectWorkbook บน workbook เก็บ hash ของ password ยาว 4 หลัก hex ไว้ นั่นคืออัลกอริทึม legacy ที่ทั้ง OOXML และ BIFF สืบทอดมาจาก Excel ยุค 1990s และเอกสารของฟอร์แมตก็ไม่เคยอ้างว่ามันทำได้มากกว่าหยุดการแก้ไขโดยไม่ตั้งใจเลย แพ็กเกจยังคงเป็น zip ที่อ่านได้ธรรมดา: ข้อมูลเซลล์ สูตร และ shared string ล้วนอยู่ใน XML แบบ cleartext ค่าเริ่มต้นยิ่งทำให้แย่กว่าเดิม ไม่ใช่ดีขึ้น ทุกเซลล์เริ่มต้นด้วย Locked=True ดังนั้นการเรียก Protect โดยไม่ปลดล็อกช่วงรับข้อมูลก่อน จะแช่แข็งทั้ง sheet ไม่ให้แก้ไขได้ ในขณะที่ยังเปิดเผยทุกค่าไว้ให้เห็นชัด ๆ

ทั้งหมดนี้ไม่ได้ทำให้ protection ไร้ประโยชน์แต่อย่างใด การนำผู้ใช้ให้อยู่ในช่วงที่แก้ไขได้ และการทำให้ layout มั่นคงสำหรับการพิมพ์ คืองานจริงที่มีประโยชน์ ครอบคลุมไว้ใน บทความของเราเรื่อง worksheet protection และ page setup แต่นั่นคืองานด้าน usability เมื่อไรก็ตามที่ความต้องการคือความลับ API เดียวที่ตอบโจทย์นั้นได้คือ SaveAsEncrypted

SaveAsEncrypted เขียนอะไรจริง ๆ กันแน่

การ implement นี้ทำตาม ECMA-376 Standard Encryption ตามที่ระบุไว้ใน [MS-OFFCRYPTO] หัวข้อ 2.3.4 password จะวิ่งผ่าน SHA-1 จำนวน 50,000 รอบ เพื่อสร้างกุญแจ AES-128 บล็อกตัวตรวจสอบที่เข้ารหัสด้วย AES-128 แบบ ECB mode จะให้ผู้บริโภคยืนยัน password ได้ก่อนที่จะถอดรหัสอะไรเลย แล้วทั้งแพ็กเกจ workbook ก็จะถูกเข้ารหัสด้วย AES-128 แบบ CBC mode สิ่งที่ลงไปอยู่บนดิสก์ไม่ใช่ zip เลยแม้แต่น้อย มันคือ OLE compound file ที่บรรจุ stream EncryptionInfo, EncryptedPackage และ DataSpaces โดยไม่มีไดเรกทอรี xl/ ให้เครื่องมือ archive แสดงรายการเลย นี่คือเหตุผลที่การทดสอบเปลี่ยนชื่อไฟล์ตอนนี้ไม่พบอะไรที่อ่านได้เลย Excel 2007 ขึ้นไปเปิดมันได้ด้วย password เพียงอย่างเดียว และ 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;

ปฏิบัติต่อตัวแปร password ด้วยความระมัดระวังเท่ากับ connection string เลย ดึงมันมาจาก vault หรือบริการสร้างความลับในนาทีสุดท้าย อย่า log มันเด็ดขาด และอย่าเขียนมันลงใน workbook เองเด็ดขาด การตรวจสอบ return code ไม่ใช่พิธีกรรมที่เลือกทำก็ได้ การ save แบบเข้ารหัสที่ล้มเหลวกลางคันต้องยกเลิกการส่งมอบ เพราะทางเลือกสำรองเดียวที่โค้ดที่เรียกมันเสนอได้คือสำเนาที่ไม่เข้ารหัส และสำเนานั้นก็คือเหตุการณ์ที่ฟีเจอร์นี้มีอยู่เพื่อป้องกันพอดี

มีการทดสอบยอมรับที่ตรวจสอบด้วยเครื่องได้ด้วย ซึ่งแทบไม่มีต้นทุนเลย: เรียก CanReadEncrypted กับไฟล์ที่คุณเพิ่งเขียน มันจะคืนค่า true ก็ต่อเมื่อผลลัพธ์เป็น container ที่เข้ารหัสจริง ๆ เท่านั้น ดังนั้นการ assert มันหลังทุกครั้งที่ save แบบเข้ารหัสจะจับ regression ที่สำคัญที่สุดได้ นั่นคือโค้ดที่แอบตกกลับไปใช้ SaveAs ธรรมดาแบบเงียบ ๆ ได้ทันทีที่มันเกิดขึ้น แทนที่จะรู้อีกหลายสัปดาห์ต่อมาในกล่องจดหมายของลูกค้า คำตัดสินสุดท้ายยังคงเป็นของการเปิดด้วยมือใน Excel ด้วย password จริงระหว่างการทดสอบก่อนออก release

เขียนได้อย่างเดียวโดยตั้งใจ: จัดการกับ EXlsxEncryptionNotImplemented

นี่คือความไม่สมมาตรที่ควรกำหนดสถาปัตยกรรม pipeline ของคุณ: HotXLS เข้ารหัสตอน save แต่ไม่ถอดรหัสตอน open OpenEncrypted จะโยน EXlsxEncryptionNotImplemented เมื่อชี้ไปที่แพ็กเกจที่เข้ารหัสจริง ๆ ส่วนกับ workbook ธรรมดา มันก็แค่ตกลงไปที่ Open ปกติเฉย ๆ ตัวตรวจสอบคู่กันชื่อ CanReadEncrypted ตรวจจับ OLE encryption container ได้ในราคาถูก ดังนั้นโค้ดรับข้อมูลเข้าก็ route ไฟล์แบบนี้ได้โดยไม่ต้องกระตุ้น exception:

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.CanReadEncrypted(FileName) then
    begin
      // Container ที่เข้ารหัส: 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;

ความไม่สมมาตรนั้นมีการตีความเชิงสถาปัตยกรรมที่ชัดเจนอยู่หนึ่งอย่าง: เข้ารหัสที่ขอบของการส่งมอบเป็นขั้นตอนสุดท้าย เก็บต้นฉบับ plaintext ไว้ภายในขอบเขตความไว้วางใจของคุณ ไม่ว่าจะเป็นฐานข้อมูล document store หรือ share ที่ควบคุมสิทธิ์เข้าถึง แล้วสร้างสำเนาที่เข้ารหัสเป็นขั้นตอนสุดท้ายก่อนไฟล์ออกจากระบบไป pipeline ที่เก็บแค่ผลลัพธ์ที่เข้ารหัสไว้เท่านั้น ได้ล็อกตัวเองออกจากข้อมูลของตัวเองไปแล้ว เพราะไม่มีขั้นตอนใดในระบบเดียวกันในภายหลังที่จะเปิดไฟล์เหล่านั้นกลับมาได้เลย เมื่อกระบวนการ HotXLS ปลายทางต้องการ workbook อีกครั้ง ให้ส่งต้นฉบับ plaintext ให้มัน ไม่ใช่ artifact ที่ส่งมอบไปแล้ว

แผนภาพไปป์ไลน์การเรียก SaveAsEncrypted ของ HotXLS บน Delphi: รหัสผ่านคลังถูกหมุนผ่าน SHA-1 50,000 รอบเป็นคีย์ AES-128, บล็อก verifier แบบ ECB และการเข้ารหัสแพ็กเกจแบบ CBC ได้ไฟล์ compound แบบ OLE ที่ตรวจด้วย CanReadEncrypted
การเรียก SaveAsEncrypted หนึ่งครั้งแปลงรหัสผ่านตู้เซฟเป็นคีย์ AES-128 และไฟล์ OLE compound CanReadEncrypted ให้การบันทึกมีประตูยอมรับที่เครื่องตรวจสอบได้

AES-128 Standard Encryption และเส้น compliance ของ AES-256

การเข้ารหัสไฟล์ของ Office มีสองรุ่น Standard Encryption ซึ่งเป็นแบบที่ HotXLS เขียน ใช้ AES-128 กับการสร้างกุญแจแบบ SHA-1 Agile Encryption มาทีหลังและย้ายไปใช้ AES-256 กับ SHA-512 และ key container ที่อธิบายด้วย XML แบบต่างออกไป ทั้งสองแบบเปิดได้ใน Excel อย่างโปร่งใส และ AES-128 ก็ยังคงมั่นคงในเชิงการคำนวณสำหรับปกป้องไฟล์ระหว่างส่งไปยังลูกค้า

แผนภาพสถาปัตยกรรมสำหรับบริการ Delphi: เวิร์กบุ๊กต้นฉบับแบบ plaintext อยู่ภายในเขตความน่าเชื่อถือ, SaveAsEncrypted ของ HotXLS ทำงานเป็นขั้นสุดท้ายที่ขอบการส่งมอบ และคำเตือนไม่ให้เก็บถาวรเฉพาะสำเนาที่เข้ารหัสแล้ว
เข้ารหัสที่ขอบการส่งมอบเป็นลำดับสุดท้าย จากต้นแบบ plaintext ที่เก็บไว้ภายในเขตความเชื่อมั่น การจัดเก็บถาวรเฉพาะสำเนาที่เข้ารหัสจะล็อกขั้นตอนถัด ๆ ไปออกจากข้อมูลของตัวเอง

ความต่างนี้จะเลิกเป็นแค่เรื่องวิชาการทันทีที่แบบสอบถามความปลอดภัยถามหา "การเข้ารหัส AES-256 สำหรับไฟล์ที่พักอยู่นิ่ง" Standard Encryption ไม่ผ่านเกณฑ์นั้น ไม่ว่า password จะแข็งแรงแค่ไหนก็ตาม และไม่มีพารามิเตอร์ใดของ SaveAsEncrypted ที่จะเปลี่ยนอัลกอริทึมที่มันปล่อยออกมาได้เลย ดังนั้นให้ระบุ profile อย่างแม่นยำในเอกสารความปลอดภัยของคุณ: AES-128, ECMA-376 Standard Encryption, การสร้างกุญแจแบบ SHA-1 ที่ 50,000 รอบ ข้อกล่าวอ้างที่รอดผ่านการรีวิวได้มีค่ามากกว่าข้อกล่าวอ้างที่มองโลกในแง่ดีแต่พังลงตอนถูก audit

เส้นทาง XLS แบบดั้งเดิม: RC4 ออกไป แต่ RC4 กับ XOR กลับเข้ามา

facade ของ BIFF มีรูปทรงตรงกันข้าม การเข้ารหัสของมันเก่ากว่าและอ่อนแอกว่า แต่ round trip นั้นครบวงจร: สิ่งที่มันเขียน มันก็อ่านกลับได้เช่นกัน การตั้งค่า EncryptionPassword ก่อน SaveAs จะสร้างไฟล์ .xls ที่เข้ารหัสด้วย RC4 ผ่านกลไก FilePass ของ BIFF และ Open พร้อมพารามิเตอร์ password จะอ่านได้ทั้งสามสกีมาแบบดั้งเดิม คือ RC4, RC4 CryptoAPI และการทำให้งงงวยด้วย XOR แบบโบราณ:

var
  Writer, Reader: IXLSWorkbook;   // interface reference: ไม่ต้อง 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 เริ่มที่ 1
end;

RC4 คือการเข้ารหัสที่ล้าสมัยแล้ว และไม่ควรใช้ปกป้องข้อมูลที่สำคัญในทุกวันนี้เด็ดขาด คุณค่าเดียวที่มันยังเหลืออยู่คือความสามารถทำงานร่วมกับระบบที่ยังแลกเปลี่ยนไฟล์ .xls อยู่ อย่างไรก็ตาม ฝั่งการอ่านนั้นคุ้มค่ามากในงาน migration ไฟล์ legacy ที่มี password ป้องกันไว้เปิดได้ด้วย Open(FileName, Password) เชื่อมเข้าสู่โมเดล OOXML แล้วรักษาความปลอดภัยใหม่ผ่านเส้นทาง AES ซึ่งเป็นการอัปเกรดทางเดียวที่รันได้โดยไม่มี Excel อยู่ใน loop เลย สำหรับการส่งมอบไฟล์เข้ารหัสปริมาณมาก บันทึกเรื่อง throughput ฝั่งเขียนใน บทความของเราเรื่อง streaming write สำหรับงาน batch บนเซิร์ฟเวอร์ ใช้ได้กับขั้นตอนสร้างเนื้อหาที่เกิดขึ้นก่อนการเข้ารหัส

การเข้ารหัสกับ protection ไม่ใช่คู่แข่งกัน

มีอีกประเด็นหนึ่งที่ควรทำความเข้าใจให้ชัด เพราะมันมักโผล่มาทันทีที่มีคนอ่านคำเตือนที่หัวหน้าเพจนี้แล้วสรุปว่า "protection ไร้ค่า" ซึ่งไม่จริง การเข้ารหัสกับ protection ตอบคำถามคนละข้อกัน และมันซ้อนกันได้อย่างสะอาด การเข้ารหัสตัดสินว่าใครเปิดไฟล์ได้ ส่วน protection ตัดสินว่าคนที่เข้ามาข้างในได้แล้วแก้ไขอะไรได้บ้าง การส่งมอบข้อมูลเงินเดือนทำทั้งสองอย่างพร้อมกันได้อย่างสมเหตุสมผล: เข้ารหัสแพ็กเกจเพื่อให้เห็นได้เฉพาะคนที่ถือ password เท่านั้น แล้วล็อกเซลล์สูตรเพื่อให้ผู้รับกรองและเรียงข้อมูลได้ แต่ไม่สามารถแอบเขียนทับการคำนวณได้ ความผิดพลาดไม่ใช่การเพิ่ม protection เข้าไป ความผิดพลาดคือการปล่อยให้การมีอยู่ของมันมาแทนที่การเข้ารหัส ทั้งที่ความต้องการจริง ๆ คือความลับ

ฝั่งการดูแลรักษา password ไม่มีตาข่ายนิรภัยรองรับเลย และนั่นคือการออกแบบที่ตั้งใจ การสร้างกุญแจแบบ 50,000 รอบมีไว้เพื่อทำให้การเดาแพงขึ้น และไม่มีอะไรในไฟล์เก็บความลับสำรองไว้เลย password หายเท่ากับข้อมูลหาย สร้าง ส่งมอบ และเก็บรักษา password เหล่านี้ด้วยวินัยเดียวกับที่คุณใช้กับ credential ของฐานข้อมูล แล้วการเข้ารหัสก็จะทำหน้าที่ของมันได้อย่างสมบูรณ์

การเข้ารหัสไฟล์จริง ๆ คือการเรียกเพียงครั้งเดียวใน HotXLS วินัยทั้งหมดอยู่ในทุกสิ่งที่ล้อมรอบการเรียกนั้น: การดูแลรักษา password ขอบเขตแบบเขียนได้อย่างเดียวที่ทำให้ HotXLS เปิดผลลัพธ์ของตัวเองกลับไม่ได้ และข้อกล่าวอ้างเรื่องอัลกอริทึมที่คุณป้องกันได้เมื่อถูก audit SaveAsEncrypted และการ round-trip แบบ legacy มาพร้อมกับ HotXLS Delphi Component ซึ่งรันแบบ native ในโพรเซส Delphi และ C++Builder โดยไม่มี Excel automation อยู่ในเส้นทางเลย