HotXLS đọc các tệp Excel mã hóa Agile (phương thức bảo vệ bằng mật khẩu được áp dụng mặc định bởi Excel 2010 và các phiên bản mới hơn) thông qua một lệnh gọi duy nhất: TXLSXWorkbook.OpenEncrypted. Thành phần này sẽ phân tích cú pháp mô tả mã hóa XML, tính toán dẫn xuất các khóa từ mật khẩu bằng chuỗi băm SHA-512 với số vòng lặp (spin-count) lớn, xác thực mật khẩu đối chiếu với bộ xác thực đã mã hóa, và sau đó giải mã gói dữ liệu theo các phân đoạn AES-CBC 4096-byte. Toàn bộ quy trình không yêu cầu cài đặt Excel, không dùng COM, và không cần DLL mã hóa bên ngoài
Bài viết này tập trung cụ thể vào phía đọc của mã hóa Agile. Hai vấn đề liên quan có các bài viết riêng của chúng: khả năng tương tác với các cơ chế RC4 và XOR cũ bên trong các tệp BIFF .xls cũ được trình bày trong bài viết về tương thích ECB và RC4, và việc tạo ra các sổ làm việc bảo vệ bằng mật khẩu với Mã hóa tiêu chuẩn ECMA-376 được mô tả trong bài viết về đầu ra XLSX bảo vệ bằng AES. Ở đây tệp tin đã tồn tại sẵn, do nguồn khác mã hóa, và nhiệm vụ của bạn là mở nó ra
Tình huống buộc bạn phải giải quyết vấn đề này rất quen thuộc với bất kỳ ai vận hành một quy trình xử lý tài liệu. Một dịch vụ nhập tệp phía máy chủ chấp nhận các sổ làm việc tải lên; máy chủ không cài đặt Excel và sẽ không bao giờ cài; rồi một buổi sáng, một khách hàng tải lên một tệp .xlsx hoàn toàn bình thường nhưng trình đọc ZIP từ chối vì nó thực chất không phải định dạng ZIP. Khách hàng đã lưu tệp đó kèm mật khẩu. Từ thời điểm đó, trình tải của bạn hoặc là phải hiểu được chuẩn [MS-OFFCRYPTO] hoặc là phải trả lại tệp lỗi cho người dùng, trong khi ở góc độ của họ, họ không làm gì bất thường cả
Mã hóa Agile trong tệp Excel là gì?
Mã hóa Agile là cơ chế bảo vệ bằng mật khẩu được định nghĩa trong [MS-OFFCRYPTO] §2.3.4.10 đến §2.3.4.15, và là những gì Excel 2010 và các bản mới hơn ghi lại mỗi khi sổ làm việc được lưu kèm mật khẩu. Tệp tin mã hóa không còn là một gói nén ZIP thông thường. Nó là một thùng chứa nhị phân OLE Compound File Binary (CFB) giữ hai luồng dữ liệu: EncryptionInfo mô tả cách thức thực hiện mã hóa, và EncryptedPackage chính là tệp ZIP .xlsx thực tế được mã hóa dưới dạng một khối mờ đục (opaque blob). Chữ ký CFB (D0 CF 11 E0 A1 B1 1A E1) giống hệt ký tự nhận dạng mà các tệp tin BIFF .xls cũ mang theo, đó là lý do tại sao một tệp tin bị đổi tên hoặc mã hóa không thể được phân loại chỉ bằng phần mở rộng tệp
Điểm phân biệt Agile với các phiên bản tiền nhiệm của nó là EncryptionInfo có tính chất tự mô tả. Sau phần tiền tố phiên bản dài 8 byte với cả phiên bản chính và phụ đều là 4, luồng dữ liệu là một mô tả XML chuẩn UTF-8. Phần tử keyData khai báo thuật toán mã hóa (AES), chế độ liên kết (ChainingModeCBC), hàm băm (SHA512), độ dài khóa theo bit, kích thước khối, và muối Base64 (salt). Phần tử mật khẩu keyEncryptor mang muối riêng của nó, số vòng lặp spinCount, và ba chuỗi Base64: encryptedVerifierHashInput, encryptedVerifierHashValue, và encryptedKeyValue. Excel thường ghi mã hóa AES-256 với số vòng lặp băm là 100,000, nhưng phần mô tả được phép khai báo AES-128 hoặc AES-192, và HotXLS tuân thủ bất kỳ giá trị nào mà keyBits chỉ định thay vì mặc định giả định là 256
Một điểm truy cập duy nhất cho các sổ làm việc dạng thuần, nén Standard và mã hóa Agile
Phương thức TXLSXWorkbook.OpenEncrypted xử lý cả ba trạng thái mà người gọi có thể gặp: tệp ZIP thông thường, mã hóa Standard và mã hóa Agile, nhờ đó các trình xử lý tải lên không cần phân loại tệp tin trước khi tải chúng. Phương thức trước tiên sẽ kiểm tra tệp: nếu không có chữ ký CFB, nó chuyển hướng sang đường dẫn Open thông thường và mật khẩu sẽ bị bỏ qua. Nếu tệp tin là một thùng chứa CFB, nó sẽ thử kiểm tra Mã hóa tiêu chuẩn ECMA-376 trước, và khi nhận diện phiên bản EncryptionInfo là Agile 4.4, nó sẽ chuyển sang quy trình xử lý Agile. Giá trị trả về là 1 khi thành công, cùng một giao ước giống như lệnh gọi Open
var
Wb: TXLSXWorkbook;
begin
Wb := TXLSXWorkbook.Create;
try
// Works for plain .xlsx, Standard-encrypted and
// Agile-encrypted files alike
if Wb.OpenEncrypted('upload.xlsx', 'customer-password') = 1 then
Writeln(VarToWideStr(Wb.Sheets[1].Cells[1, 1].Value));
finally
Wb.Free;
end;
end;
Khả năng tự động chuyển đổi dự phòng sang đầu vào không mã hóa quan trọng hơn vẻ bề ngoài của nó. Một trình nhập hàng loạt luôn gọi OpenEncrypted sẽ không cần phân nhánh mã nguồn tại nơi gọi: các tệp tin chưa từng được bảo vệ sẽ tải chính xác như trước đây, và các tệp tin gửi đến dưới dạng mã hóa sẽ được giải mã trực tiếp tại chỗ rồi chuyển tới trình tải ZIP thông thường dưới dạng một luồng trong bộ nhớ. Bạn chỉ cần kiểm thử một đường dẫn mã duy nhất thay vì ba đường dẫn
Mật khẩu trở thành khóa AES như thế nào?
Mã hóa Agile không bao giờ sử dụng trực tiếp mật khẩu. HotXLS trước hết sẽ tính toán một mã băm lặp: mã băm ban đầu là kết quả SHA-512 trên muối mật khẩu kết hợp với các byte UTF-16LE của mật khẩu, sau đó kết quả băm được băm lại spinCount lần, mỗi vòng lặp được chèn thêm bộ đếm vòng lặp little-endian 32-bit vào trước kết quả băm trước đó. Với số vòng lặp mặc định của Excel là 100,000, quy trình sẽ thực hiện một trăm nghìn lần gọi SHA-512 liên tục cho mỗi lượt thử mật khẩu, và đó chính là mấu chốt của vấn đề. Số vòng lặp băm là bộ kiểm soát lực lượng thô (brute-force throttle): nó chỉ tốn vài mili giây của người dùng hợp lệ cho một lần mở, nhưng bắt buộc kẻ tấn công từ điển phải tiêu tốn đúng khoảng thời gian đó cho mỗi lượt đoán mật khẩu
// [MS-OFFCRYPTO] iterated password hash:
// H(0) = SHA-512(salt + UTF-16LE(password))
// H(n) = SHA-512(LE32(n - 1) + H(n - 1)), repeated spinCount times
function AgilePasswordHash(const Password: WideString;
const Salt: TBytes; SpinCount: Integer): TBytes;
var
buf: TBytes;
i: Integer;
begin
Result := XlsSHA512(Concat(Salt, Utf16LEBytes(Password)));
SetLength(buf, 4 + 64);
for i := 0 to SpinCount - 1 do
begin
PutLE32(buf, 0, i); // iteration counter, little-endian
Move(Result[0], buf[4], 64); // previous digest
Result := XlsSHA512(buf);
end;
end;
Khóa băm lặp vẫn chưa phải là khóa thực tế. Ba khóa khác biệt được dẫn xuất từ nó bằng cách thực hiện băm thêm một lần với một khóa khối cố định 8 byte được nối thêm vào sau, mỗi hằng số tương ứng với một mục đích: FE A7 D2 76 3B 4B 9E 79 dùng để giải mã dữ liệu đầu vào của bộ xác thực, D7 AA 0F 6D 30 61 34 4E dùng để băm giá trị verifier, và 14 6E 0B E7 AB AC D0 D6 dùng để gỡ bỏ lớp mã hóa của khóa gói dữ liệu thực tế. Mỗi kết quả SHA-512 được cắt ngắn về độ dài khóa đã khai báo, và theo đặc tả [MS-OFFCRYPTO], được chèn thêm các byte 0x36 trong trường hợp kết quả băm ngắn hơn chiều dài khóa. Quy tắc chèn đệm 0x36 tương tự cũng áp dụng khi muối mật khẩu được mở rộng để đạt kích thước khối làm vector khởi tạo (IV) cho chế độ CBC
Xác thực mật khẩu và cái bẫy cắt ngắn saltSize
HotXLS xác thực mật khẩu trước khi chạm vào gói dữ liệu, sử dụng cặp đối chiếu (verifier pair) từ mô tả mã hóa. Nó giải mã encryptedVerifierHashInput bằng khóa dẫn xuất thứ nhất, băm kết quả bằng SHA-512, giải mã encryptedVerifierHashValue bằng khóa dẫn xuất thứ hai, và so sánh hai kết quả băm từng byte một. Sự không khớp đồng nghĩa với mật khẩu sai, được báo cáo thành một kết quả lỗi riêng biệt thay vì tạo ra một sổ làm việc bị lỗi dữ liệu rác, và điều quan trọng là phần thân của gói dữ liệu không bao giờ bị giải mã bằng một khóa sai, tránh tình trạng mật khẩu sai lại tạo ra các dữ liệu bị hỏng trông có vẻ hợp lệ
Có một chi tiết đặc tả ở đây rất dễ bị làm sai. Mục [MS-OFFCRYPTO] §2.3.4.13 định nghĩa verifier là saltSize byte dữ liệu ngẫu nhiên, trong đó saltSize là độ dài muối của trình mã hóa khóa, chứ không phải kích thước khối thuật toán (cipher block size). Vì bản mã AES-CBC được căn chỉnh theo khối, dữ liệu đầu vào verifier sau khi giải mã sẽ có thêm đệm để đạt bội số của 16 byte, và nó phải được cắt ngắn về đúng độ dài saltSize trước khi băm. Excel luôn ghi saltSize bằng blockSize, cả hai đều là 16, so với một triển khai bỏ qua bước cắt ngắn này sẽ vượt qua mọi bài kiểm thử với tệp Excel thực tế nhưng lại thất bại ngay lập tức ở tệp tin đầu tiên từ một công cụ tạo tệp chọn độ dài muối khác. HotXLS thực hiện cắt ngắn về độ dài muối vì đó là những gì đặc tả kỹ thuật quy định, và việc hai giá trị này bằng nhau trong thực tế chỉ là sự trùng hợp ngẫu nhiên, không phải một điều khoản ràng buộc
Phân đoạn EncryptedPackage được giải mã như thế nào?
Luồng EncryptedPackage bắt đầu bằng độ dài văn bản thuần 8 byte dạng little-endian, tiếp theo là bản mã được chia thành các phân đoạn 4096-byte, và HotXLS thực hiện giải mã từng phân đoạn một với một IV mới cho mỗi phân đoạn. Bản thân khóa của gói dữ liệu không được dẫn xuất từ mật khẩu: nó là một khóa trung gian ngẫu nhiên mà trình viết đã mã hóa vào mục encryptedKeyValue, và HotXLS thực hiện gỡ bỏ lớp mã hóa của nó bằng khóa dẫn xuất thứ ba, cắt ngắn về độ dài khóa được khai báo bởi keyData. Mỗi giá trị IV của phân đoạn là kết quả SHA-512 trên muối của keyData kết hợp với chỉ số phân đoạn little-endian 32-bit, được cắt ngắn về kích thước khối. Cấu trúc đó cho phép giải mã độc lập bất kỳ phân đoạn 4096-byte nào, về nguyên lý giúp định dạng này rất thân thiện với việc truy cập ngẫu nhiên, mặc dù HotXLS giải mã toàn bộ gói dữ liệu vào bộ nhớ và chuyển các byte ZIP kết quả tới trình tải XLSX thông thường của nó
Độ dài văn bản thuần được khai báo sẽ hoàn tất phần việc cuối cùng. Kết quả AES-CBC luôn được căn chỉnh theo khối, do đó phân đoạn cuối cùng mang theo tối đa 15 byte đệm không thuộc về tài liệu; bộ đệm đã giải mã được cắt ngắn về giá trị tiền tố kích thước, thu lại chính xác tệp ZIP .xlsx mà Excel đã mã hóa. HotXLS xác thực tiền tố đối chiếu với độ dài thực tế của luồng trước khi giải mã, nhờ đó một lượt tải tệp bị cắt ngắn hoặc một trường kích thước bị sửa đổi trái phép sẽ báo lỗi sạch sẽ thay vì gây ra lỗi tràn dữ liệu
Báo cáo lỗi và giới hạn thực tế
Các chế độ lỗi được tách biệt một cách có chủ ý. Mật khẩu sai sẽ kích hoạt một ngoại lệ với thông điệp sai mật khẩu rõ ràng, được xác định từ việc không trùng khớp của bộ xác thực, giúp giao diện người dùng có thể nhắc nhở người dùng thử lại. Một thùng chứa CFB có phần mô tả khai báo các thuật toán nằm ngoài tập hợp hỗ trợ, ví dụ bất kỳ thứ gì khác ngoài AES với liên kết CBC và băm SHA-512 trong một mô tả Agile, hoặc một thùng chứa không phải định dạng Standard hay Agile, sẽ kích hoạt một ngoại lệ khác xác định cơ chế đó chưa được hỗ trợ. Hai lỗi này tuyệt đối không được gộp làm một: việc yêu cầu người dùng thử lại mật khẩu trên một cơ chế chưa hỗ trợ làm lãng phí thời gian của họ, và việc báo cáo sai mật khẩu thành lỗi định dạng tệp sẽ định hướng sai cho đội ngũ hỗ trợ kỹ thuật của bạn
function LoadUploadedWorkbook(const FileName: WideString;
const Password: WideString; Wb: TXLSXWorkbook): Boolean;
begin
Result := False;
try
Result := Wb.OpenEncrypted(FileName, Password) = 1;
except
on E: EXlsxEncryptionNotImplemented do
// Raised for both a wrong password and an unsupported
// scheme; E.Message states which, so log it verbatim and
// only offer a password retry for the wrong-password case
RejectUpload(FileName, E.Message);
end;
end;
Các giới hạn cần được nêu rõ ràng. HotXLS đọc các mô tả Agile khai báo AES trong chế độ CBC với SHA-512, đáp ứng những gì Excel 2010 đến Excel 365 thực tế ghi ra, ở cả ba kích thước khóa. Các mô tả khai báo thuật toán mã hóa hoặc thuật toán băm khác sẽ bị từ chối thay vì suy đoán thử nghiệm, và các khóa mã hóa dựa trên chứng thư số không được truy vấn, chỉ hỗ trợ trình mã hóa khóa dựa trên mật khẩu. Ở phía ghi tài liệu, HotXLS hiện tại tạo ra Mã hóa tiêu chuẩn (Standard Encryption) chứ không phải Agile, một điểm khác biệt cần lưu ý nếu các công cụ ở hạ nguồn kiểm tra cơ chế mã hóa; chi tiết được mô tả trong biết về ghi đầu ra XLSX bảo vệ bằng AES
Việc tải lên các tệp được bảo vệ bằng mật khẩu sẽ không còn là một trường hợp đặc biệt khó xử lý một khi trình tải coi việc mã hóa là một phần của định dạng tệp chứ không phải là một ngoại lệ. Điểm truy cập OpenEncrypted, thuật toán dẫn xuất băm spin-count SHA-512 và quy trình AES-CBC phân đoạn được mô tả ở đây đi kèm như một phần của HotXLS Delphi Excel Component, bên cạnh các tính năng đọc và ghi XLS và XLSX gốc khác dành cho Delphi và C++Builder