Bạn ghi một sổ làm việc, mã hóa nó bằng một mật khẩu, giao tệp tin cho một đồng nghiệp, và đồng nghiệp mở nó trong Excel. Excel yêu cầu nhập mật khẩu. Đồng nghiệp gõ mật khẩu, và Excel chấp nhận nó. Cho đến nay việc mã hóa có vẻ chính xác. Sau đó Excel đưa ra một hộp thoại thông báo rằng tệp tin bị hỏng và không thể mở được, hoặc nó mở ra một trang tính chứa các ô không có ý nghĩa. Mật khẩu nhập vào đúng. Tệp tin vẫn bị hỏng. Đây là kịch bản thất bại gây mất phương hướng nhất trong việc mã hóa Office, bởi vì phần giúp bạn xác nhận mật khẩu đúng và phần chứa dữ liệu của bạn được bảo vệ bởi hai thao tác khác nhau, và việc thực hiện chính xác thao tác này không đảm bảo gì cho thao tác kia.
Cả hai lỗi mô tả ở đây đều có cấu trúc này. Trong mỗi trường hợp, phần xác thực (verifier) đã vượt qua và phần thân tài liệu thì không, điều này khiến bạn mất thời gian tìm kiếm lỗi nhập mật khẩu hoặc lỗi dẫn xuất khóa vốn không tồn tại ở đó. Lỗi thực sự nằm ở phía hạ nguồn, trong cách các byte của gói dữ liệu được biến đổi. Two faults are independent, one in the AES path and one in the RC4 path, but they share a diagnosis problem, so it is worth seeing why a half-correct result is the hardest kind to read.
Tại sao mật khẩu được chấp nhận không chứng minh được gì về phần thân tài liệu
Định dạng mà tệp XLSX mã hóa hiện đại sử dụng là Mã hóa Tiêu chuẩn ECMA-376 (ECMA-376 Standard Encryption), và nó lưu trữ hai thứ được mã hóa nằm cạnh nhau. Một là EncryptionVerifier: một khối dữ liệu nhỏ chứa một giá trị ngẫu nhiên và mã băm của giá trị đó, được mã hóa bằng khóa rút ra từ mật khẩu. Thứ còn lại là EncryptedPackage: toàn bộ vùng chứa dạng nén zip của sổ làm việc, được mã hóa bằng cùng một khóa. Phần xác thực tồn tại để trình đọc có thể xác nhận mật khẩu trước khi nó dành tài nguyên để xử lý megabyte dữ liệu của phần thân tài liệu. Giải mã phần xác thực, băm giá trị ngẫu nhiên, so sánh nó với mã băm được lưu trữ, và nếu chúng khớp nhau thì mật khẩu là chính xác.
Bẫy ở đây là phần xác thực và gói dữ liệu được mã hóa bằng các lệnh gọi riêng biệt trên các bộ đệm riêng biệt. Một khóa được dẫn xuất chính xác sẽ giải mã phần xác thực chính xác bất kể điều gì xảy ra với gói dữ liệu sau đó. Vì vậy, nếu quy trình dẫn xuất khóa của bạn đúng nhưng chuyển đổi gói dữ liệu của bạn sai, Excel sẽ xác nhận mật khẩu từ phần xác thực và sau đó thất bại ở phần thân tài liệu. Triệu chứng được đọc là "mật khẩu đúng, tệp hỏng", điều này hướng cuộc điều tra vào đường dẫn mật khẩu, vốn là phần duy nhất chưa bao giờ bị lỗi. Sự phân tách tương tự cũng kiểm soát trường hợp RC4 kế thừa: mã băm xác thực được kiểm tra trước, và một phần thân bị lệch pha vẫn giúp phép kiểm tra đó thành công.
Lỗi thứ nhất: AES ở chế độ ECB, không phải CBC
[MS-OFFCRYPTO] §2.3.4.15 quy định rằng Mã hóa Tiêu chuẩn sẽ mã hóa gói dữ liệu bằng thuật toán AES ở chế độ Electronic Codebook (ECB). Mọi khối 16-byte của gói được đệm đều được mã hóa độc lập bằng cùng một khóa. Không có sự liên kết chuỗi (chaining) giữa các khối và không có vectơ khởi tạo (initialization vector). Đây là một lựa chọn bất thường theo các thế hệ tiêu chuẩn hiện đại, nơi ECB thường bị tránh, nhưng việc tương thích hệ thống không phải là nơi để đặt câu hỏi ngược lại đặc tả. Excel giải mã gói dữ liệu dưới dạng ECB, do đó bộ tạo lập phải mã hóa nó dưới dạng ECB nếu không hai bên sẽ không đồng nhất dữ liệu.
Lỗi xảy ra do gói dữ liệu được mã hóa bằng AES ở chế độ CBC sử dụng một vectơ khởi tạo chứa toàn số không. Đây là lý do tại sao nó gần như hoạt động, và tại sao "gần như" lại là nơi tồi tệ nhất để hạ cánh. Trong CBC, khối văn bản rõ đầu tiên được XOR với IV trước khi mã hóa. Khi IV chứa toàn số không, phép XOR đó không thay đổi gì, do đó khối đầu tiên của CBC-với-IV-toàn-không tạo ra chính xác bản mã giống như ECB. Từ khối thứ hai trở đi, CBC nạp khối bản mã trước đó vào khối tiếp theo, vì vậy mọi khối sau khối đầu tiên đều phân kỳ khỏi ECB.
Bây giờ hãy phủ điều đó lên cấu trúc. Bố cục gói đặt một tiền tố độ dài byte endian nhỏ 8-byte ở ngay đầu, do đó các phần của tệp Excel kiểm tra sớm nhất nằm ở khối đầu tiên hoặc khối thứ hai. Một khối đầu tiên khớp nghĩa là các kiểm tra xác thực ban đầu đều vượt qua trong khi mọi khối sau đó giải mã ra dữ liệu nhiễu. Giải pháp sửa lỗi không có gì tinh vi một khi chế độ được chỉ tên: hãy mã hóa từng khối 16-byte bằng ECB và ngừng liên kết chuỗi. Trong trình xử lý, XlsEncryptStdPackage duyệt qua bộ đệm được đệm theo các bước 16-byte và gọi AESEncryptECB128Block trên mỗi khối, đây chính là nguyên bản được sử dụng cho các khối xác thực. Mã nguồn mang một chú thích tại vòng lặp nêu rõ quy tắc: CBC với IV toàn không chỉ khớp với ECB ở khối đầu tiên, do đó phần còn lại của gói sẽ giải mã ra dữ liệu rác và Excel sẽ từ chối nó.
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create(nil);
try
Book.Open('report.xlsx');
// SaveAsEncrypted serializes the workbook, then runs the
// ECMA-376 Standard Encryption pipeline: AES-128 ECB over the
// package per [MS-OFFCRYPTO] 2.3.4.15. Returns 1 on success.
if Book.SaveAsEncrypted('report_secure.xlsx', 'S3cret!') <> 1 then
raise Exception.Create('Encryption failed');
finally
Book.Free;
end;
end;
Lỗi thứ hai: khóa lại RC4 bị lệch pha
Đường dẫn .xls kế thừa sử dụng cơ chế RC4 CryptoAPI, và quy tắc của nó có điểm khác biệt. [MS-OFFCRYPTO] §2.3.6 quy định rằng mật mã được khóa lại (re-keyed) tại mỗi ranh giới khối 1024-byte. Luồng dữ liệu được chia thành các khối 1024 byte, một khóa RC4 mới được dẫn xuất cho khối số 0, 1, 2, v.v., và trong mỗi khối, luồng khóa được tiêu thụ liên tục từ byte này sang byte khác. Hai bất biến phải được duy trì đồng thời: khóa lại trên mỗi ranh giới, và tiêu thụ luồng khóa không có khoảng trống bên trong một khối. RC4 là một mật mã luồng, do đó luồng khóa của nó là một chuỗi tuần tự có thứ tự; byte thứ n bạn rút ra được xác định bởi số lượng byte bạn đã rút ra trước đó. Giải mã là cùng một phép XOR đối với cùng một chuỗi tuần tự, có nghĩa là bên tạo lập và bên tiêu thụ phải rút ra chính xác các byte giống nhau tại các vị trí giống nhau.
Đó là toàn bộ khó khăn. Một mật mã luồng không có tính năng tái đồng bộ hóa. Nếu bạn lãng phí một byte của luồng khóa, mọi byte sau nó đều được XOR với byte luồng khóa sai, và lỗi không bao giờ tự sửa đổi; nó xếp chồng lên nhau đến cuối khối và, một khi vị trí chạy bị sai, lan tới mọi khối sau đó. Lỗi ở đây đã làm chính xác điều đó. Bộ đếm khối bắt từ một giá trị lính gác bằng âm một, và thủ tục bỏ qua (skip routine) giả định bộ đếm đã khớp với khối hiện tại. Bắt đầu từ lính gác đó, nó đã khóa lại và chạy một khối 1024-byte luồng khóa lẽ ra không bao giờ được tiêu thụ, và trong quá trình này nó làm số lượng còn lại chuyển sang âm. Từ thời điểm đó, bộ giải mã đã bị lệch pha hoàn toàn một khối. Phần xác thực, được kiểm tra trước khi diễn ra điều này, vẫn vượt qua, do đó mật khẩu trông có vẻ đúng trong khi mọi ô dữ liệu hiển thị ra dưới dạng rác rưởi.
Logic được sửa đổi nằm trong TXLSDecrypterRC4. Cả Skip và Decrypt đều chia sẻ một vòng lặp: chỉ khóa lại khi vị trí chạy vượt qua một khối mới, trong đó chỉ mục khối là vị trí chia cho REKEY_BLOCK_SIZE (1024), sau đó tiêu thụ tối đa phần còn lại của khối hiện tại và không tiêu thụ thêm. MakeKey được gọi với chỉ mục khối, không bao giờ dùng một chỉ mục cũ hoặc lính gác, và vị trí tiến lên theo số byte chính xác được xử lý để Skip và Decrypt duy trì sự căn chỉnh pha với bên tạo lập. Bài học nằm ở đơn vị nhỏ nhất: một byte bị lãng phí không phải là một lỗi nhỏ trong mật mã luồng, nó là sự mất mát hoàn toàn của mọi thứ phía sau.
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create(nil);
try
// CanReadEncrypted checks the Compound File (OLE2) signature so
// you can branch before attempting a normal Open. OpenEncrypted
// routes plain files to Open and handles the encrypted container.
if Book.CanReadEncrypted('legacy.xls') then
Book.OpenEncrypted('legacy.xls', 'S3cret!')
else
Book.Open('legacy.xls');
// read cells here
finally
Book.Free;
end;
end;
Tương thích với một đặc tả đóng băng nghĩa là phải khớp đến từng byte
Cả hai lỗi đều quy về cùng một nguyên lý cốt lõi, và điều đó rất đáng để nêu ra vì nó thay đổi cách bạn cân nhắc các lựa chọn thiết kế. Khi bên tiêu thụ kết quả đầu ra của bạn là một chương trình bên ngoài cố định bạn không thể thay đổi, chế độ mật mã và tần suất khóa lại không phải là các chi tiết triển khai bạn có thể tối ưu hóa hay đơn giản hóa. Chúng là một phần của thỏa thuận đường truyền. Excel sẽ giải mã bằng chế độ ECB và khóa lại trên các ranh giới 1024-byte cho dù các lựa chọn đó có làm hài lòng bạn hay không, và nhiệm vụ duy nhất của bạn là tạo ra các byte có thể giải mã về dữ liệu gốc theo đúng quy trình đó. Một chế độ hiện đại hơn, một IV có vẻ vô hại, một bộ đếm bắt đầu ở nơi có cảm giác tự nhiên; bất kỳ điều nào trong số này đều là một lỗi ngay khi nó phân kỳ khỏi những gì trình đọc mong đợi. Tương thích với một đặc tả đã đóng băng không mang tính xấp xỉ. Nó phải chính xác đến từng byte hoặc nó bị hỏng.
Đây cũng là lý do tại sao phần xác thực là một bài kiểm tra nhanh kém hiệu quả nếu đứng riêng lẻ. Nó chỉ cho bạn biết việc dẫn xuất khóa hoạt động, điều này là cần thiết nhưng chưa đủ. Một bài kiểm tra chỉ mở một tệp đã mã hóa và xác nhận mật khẩu vượt qua sẽ báo cáo thành công trong khi phần thân tài liệu không thể đọc được. Một bài kiểm tra thực tế phải giải mã gói dữ liệu và so sánh các byte được phục hồi với đầu vào ban đầu, hoặc khứ hồi một sổ làm việc qua mã hóa và giải mã rồi đọc lại các ô dữ liệu. Phần xác thực chứng minh mật khẩu; chỉ có phần thân chứng minh việc mã hóa.
Cách thức hỗ trợ để đọc và ghi các sổ làm việc được bảo vệ
Bề mặt công khai là rất nhỏ. Để ghi một sổ làm việc hiện đại được bảo vệ bằng mật khẩu, hãy điền dữ liệu hoặc mở một lớp TXLSXWorkbook và gọi SaveAsEncrypted với tên tệp và một mật khẩu; nó sẽ tuần tự hóa sổ làm việc và chạy đường dẫn Mã hóa Tiêu chuẩn mà cách khắc phục đầu tiên đã sửa đổi, trả về 1 khi thành công. Để đọc, hãy gọi CanReadEncrypted để kiểm tra xem một tệp có phải là một vùng chứa Compound File đã mã hóa hay không, sau đó phân nhánh: OpenEncrypted xử lý đường dẫn mã hóa và quay lại Open cho các tệp thông thường, và lệnh Open với một mật khẩu khả dụng trực tiếp. Việc xử lý chế độ và vòng lặp khóa lại mô tả ở trên nằm bên dưới các lệnh gọi này; bạn cung cấp mật khẩu cùng tên tệp và trình xử lý sẽ đáp ứng đặc tả thay bạn.
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create(nil);
try
Book.Open('quarterly.xlsx');
Book.SaveAsEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
// Reopen on the consumer side
Book.OpenEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
finally
Book.Free;
end;
end;
Cấu trúc của đầu ra được bảo vệ, luồng EncryptionInfo, các khối xác thực, và bố cục gói được trình bày trong hướng dẫn của chúng tôi về đầu ra XLSX được bảo vệ bằng AES. Đối với câu hỏi riêng biệt về khóa ở cấp trang tính và cách bảo vệ tương tác với thiết lập trang và in ấn, xem bài viết về bảo vệ, thiết lập trang và in ấn. Cả hai đều được xây dựng dựa trên đường dẫn mã hóa được mô tả ở đây, vốn được phát hành như một phần của thành phần bảng tính HotXLS dành cho Delphi và C++Builder bên cạnh các API đọc, ghi và kết xuất được đề cập ở những bài viết khác trên blog này.