Anda menulis workbook, mengenkripsinya dengan kata sandi, menyerahkan file tersebut ke kolega, dan kolega tersebut membukanya di Excel. Excel meminta kata sandi. Kolega Anda mengetiknya, dan Excel menerimanya. Sejauh ini enkripsi terlihat benar. Kemudian Excel memunculkan dialog yang menyatakan bahwa file tersebut rusak dan tidak dapat dibuka, atau terbuka dengan lembaran sel yang tidak berarti. Kata sandinya benar. Namun file tersebut tetap rusak. Ini adalah mode kegagalan yang paling membingungkan dalam enkripsi Office, karena bagian yang memberi tahu Anda bahwa kata sandi tersebut benar dan bagian yang menampung data Anda dilindungi oleh dua operasi yang berbeda, dan keberhasilan pada salah satu bagian tidak menjamin keberhasilan bagian lainnya.
Kedua bug yang dijelaskan di sini memiliki bentuk yang persis seperti ini. Dalam setiap kasus, verifikator lolos dan badan (body) dokumen tidak, yang membuat Anda mencari bug kata sandi atau penurunan kunci (key-derivation) yang sebenarnya tidak ada. Kesalahan sebenarnya ada di hilir, dalam bagaimana byte paket ditransformasikan. Kedua kesalahan tersebut saling independen, satu di jalur AES dan satu lagi di jalur RC4, tetapi mereka berbagi masalah diagnosis yang sama, jadi penting untuk melihat mengapa hasil yang setengah benar adalah jenis yang paling sulit untuk dianalisis.
Mengapa kata sandi yang lolos tidak membuktikan apa-apa tentang badan dokumen
Format yang digunakan XLSX terenkripsi modern adalah ECMA-376 Standard Encryption, dan ia menyimpan dua hal terenkripsi secara berdampingan. Salah satunya adalah EncryptionVerifier: blok kecil yang menampung nilai acak dan hash dari nilai tersebut, dienkripsi dengan kunci yang diturunkan dari kata sandi. Yang lainnya adalah EncryptedPackage: seluruh wadah zip dari workbook, dienkripsi dengan kunci yang sama. Verifikator ada agar pembaca dapat mengonfirmasi kata sandi sebelum menghabiskan upaya pada megabyte bagian badan dokumen. Dekripsi verifikator, hash nilai acak, bandingkan dengan hash yang disimpan, dan jika cocok maka kata sandi tersebut benar.
Jebakannya adalah bahwa verifikator dan paket dienkripsi oleh pemanggilan terpisah pada buffer terpisah. Kunci yang diturunkan dengan benar akan mendekripsi verifikator dengan benar tidak peduli apa yang terjadi pada paket setelahnya. Jadi jika penurunan kunci Anda benar tetapi transformasi paket Anda salah, Excel mengonfirmasi kata sandi dari verifikator lalu gagal pada bagian badan dokumen. Gejalanya terbaca sebagai "kata sandi benar, file rusak", yang mengarahkan penyelidikan ke jalur kata sandi, yang merupakan satu-satunya bagian yang tidak pernah rusak. Pemisahan yang sama mengatur kasus RC4 lama: hash verifikator diperiksa terlebih dahulu, dan bagian badan yang keluar dari sinkronisasi tetap membuat pemeriksaan tersebut utuh.
Bug satu: AES dalam ECB, bukan CBC
[MS-OFFCRYPTO] §2.3.4.15 menentukan bahwa Standard Encryption mengenkripsi paket dengan AES dalam mode Electronic Codebook. Setiap blok 16-byte dari paket yang diberi bantalan (padded) dienkripsi secara independen dengan kunci yang sama. Tidak ada perantaian (chaining) di antara blok dan tidak ada vektor inisialisasi (initialization vector). Ini adalah pilihan yang tidak biasa menurut standar modern, di mana ECB biasanya dihindari, tetapi interop bukanlah tempat untuk meragukan spesifikasi. Excel mendekripsi paket sebagai ECB, jadi produsen harus mengenkripsinya sebagai ECB atau keduanya tidak akan cocok.
Bugnya adalah bahwa paket dienkripsi dengan AES dalam mode CBC menggunakan vektor inisialisasi semua-nol. Inilah mengapa cara tersebut hampir berhasil, dan mengapa "hampir" adalah tempat terburuk untuk mendarat. Di CBC, blok plaintext pertama di-XOR dengan IV sebelum enkripsi. Ketika IV semuanya nol, XOR tersebut tidak mengubah apa pun, sehingga blok pertama dari CBC-dengan-zero-IV menghasilkan ciphertext yang persis sama dengan ECB. Dari blok kedua dan seterusnya, CBC memasukkan blok ciphertext sebelumnya ke blok berikutnya, sehingga setiap blok setelah blok pertama berbeda dari ECB.
Sekarang terapkan itu pada struktur. Tata letak paket menempatkan awalan panjang little-endian 8-byte di awal, sehingga bagian file yang paling awal diperiksa Excel berada di blok pertama atau kedua. Blok pertama yang kebetulan cocok berarti validasi paling awal lolos sementara setiap blok berikutnya terdekripsi menjadi noise. Perbaikannya tidak rumit setelah mode tersebut diketahui: enkripsi setiap blok 16-byte dengan ECB dan hentikan perantaian. Di dalam mesin, XlsEncryptStdPackage menelusuri buffer ber-bantalan dalam langkah 16-byte dan memanggil AESEncryptECB128Block pada masing-masing blok, yang merupakan primitif yang sama yang sudah digunakan untuk blok verifikator. Kode sumber membawa komentar pada loop yang menyatakan aturan dengan jelas: CBC dengan IV nol hanya cocok dengan ECB untuk blok pertama, sehingga sisa paket akan terdekripsi menjadi sampah dan Excel akan menolaknya.
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;
Bug dua: pembuatan ulang kunci (re-key) RC4 meleset dari jalurnya
Jalur .xls lama menggunakan skema RC4 CryptoAPI, dan aturannya berbeda jenis. [MS-OFFCRYPTO] §2.3.6 menentukan bahwa cipher dibuat ulang kuncinya (re-keyed) pada setiap batas blok 1024-byte. Stream dibagi menjadi blok-blok berukuran 1024 byte, kunci RC4 baru diturunkan untuk blok nomor 0, 1, 2, dan seterusnya, dan di dalam setiap blok keystream dikonsumsi terus-menerus dari byte ke byte. Dua invariant harus dipertahankan bersama: re-key pada setiap batas, dan konsumsi keystream tanpa celah di dalam blok. RC4 adalah stream cipher, sehingga keystream-nya berupa urutan teratur tunggal; byte ke-n yang Anda tarik ditentukan oleh berapa banyak byte yang telah Anda tarik sebelumnya. Dekripsi adalah XOR yang sama terhadap urutan yang sama, yang berarti produsen dan konsumen harus menarik byte yang persis sama pada posisi yang persis sama.
Itulah seluruh kesulitannya. Stream cipher tidak memiliki sinkronisasi ulang. Jika Anda membuang satu byte keystream, setiap byte setelahnya akan di-XOR terhadap byte keystream yang salah, dan kesalahan tersebut tidak akan pernah memperbaiki dirinya sendiri; ia mengalir ke ujung blok dan, setelah posisi berjalan salah, ke setiap blok setelahnya. Bug di sini melakukan hal itu. Pencacah blok dimulai dari nilai sentinel minus satu, dan rutinitas lewati (skip) mengasumsikan pencacah sudah cocok dengan blok saat ini. Mulai dari sentinel itu, ia melakukan re-key dan menjalankan blok keystream 1024-byte penuh yang seharusnya tidak pernah dikonsumsi, dan dalam prosesnya ia membuat sisa hitungan menjadi negatif. Dari titik itu dekriptor berada satu blok penuh di luar fase. Verifikator, yang diperiksa sebelum semua ini terjadi, tetap lolos, sehingga kata sandi terlihat benar sementara setiap sel data keluar sebagai sampah.
Logika yang diperbaiki berada di TXLSDecrypterRC4. Baik Skip maupun Decrypt berbagi satu loop: lakukan re-key hanya ketika posisi berjalan melintasi blok baru, di mana indeks blok adalah posisi dibagi oleh REKEY_BLOCK_SIZE (1024), lalu konsumsi hingga sisa blok saat ini dan tidak lebih. MakeKey dipanggil dengan indeks blok, jangan pernah dengan indeks usang atau sentinel, dan posisi bertambah sebesar jumlah byte yang tepat yang diproses sehingga Skip dan Decrypt tetap selaras fase dengan produsen. Pelajaran berharga ada pada unit terkecil: satu byte terbuang bukan sekadar kesalahan kecil pada stream cipher, melainkan hilangnya seluruh data di hilir secara total.
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;
Interop dengan spesifikasi beku berarti pencocokan hingga tingkat byte
Kedua bug bermuara pada prinsip dasar yang sama, dan hal ini layak untuk dinyatakan sendiri karena mengubah cara Anda menimbang pilihan desain. Ketika konsumen dari output Anda adalah program eksternal tetap yang tidak dapat Anda ubah, mode cipher dan irama re-key bukanlah detail implementasi yang dapat Anda optimalkan atau sederhanakan. Mereka adalah bagian dari kontrak transmisi. Excel akan mendekripsi dengan ECB dan melakukan re-key pada batas 1024-byte terlepas dari apakah pilihan tersebut menyenangkan Anda atau tidak, dan satu-satunya tugas Anda adalah menghasilkan byte yang terdekripsi ke dokumen asli di bawah prosedur persis tersebut. Mode yang lebih modern, IV yang tampak tidak berbahaya, pencacah yang dimulai dari tempat yang dirasa alami; salah satu dari hal ini adalah cacat saat ia menyimpang dari apa yang diharapkan oleh pembaca. Interoperabilitas terhadap spesifikasi yang beku tidaklah perkiraan. Ini harus tepat per byte atau ia rusak.
Ini juga alasan mengapa verifikator adalah uji coba (smoke test) yang buruk jika berdiri sendiri. Ini memberi tahu Anda bahwa penurunan kunci berhasil, yang memang diperlukan tetapi jauh dari cukup. Pengujian yang hanya membuka file terenkripsi dan mengonfirmasi bahwa kata sandinya lolos akan melaporkan keberhasilan sementara bagian badan dokumen tidak terbaca. Pengujian nyata mendekripsi paket dan membandingkan byte yang dipulihkan dengan input asli, atau memproses pulang-pergi workbook melalui enkripsi dan dekripsi serta membaca sel kembali. Verifikator membuktikan kata sandi; hanya bagian badan yang membuktikan enkripsi.
Cara yang didukung untuk membaca dan menulis workbook yang dilindungi
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;
Bentuk output yang dilindungi, stream EncryptionInfo, blok verifikator, dan tata letak paket dibahas dalam panduan kami tentang output XLSX yang dilindungi-AES. Untuk pertanyaan terpisah tentang penguncian tingkat lembar (sheet-level locking) dan bagaimana perlindungan berinteraksi dengan pengaturan halaman dan pencetakan, lihat artikel tentang perlindungan, pengaturan halaman, dan pencetakan. Keduanya dibangun di atas jalur enkripsi yang dijelaskan di sini, yang dikirimkan sebagai bagian dari komponen spreadsheet HotXLS untuk Delphi dan C++Builder, bersama dengan API membaca, menulis, dan merender yang dibahas di bagian lain di blog ini.