Perpustakaan kriptografi PHP kontemporer sekitar tahun 2023.
Ringkasan:
Perpustakaan ini adalah pembungkus di sekitar pustaka natrium PHP dan pustaka PHP OpenSSL.
Kode natrium di perpustakaan ini didasarkan pada contoh yang diberikan dalam dokumentasi untuk fungsi PHP Sodium_Crypto_Secretbox ().
Kode OpenSSL di pustaka ini didasarkan pada contoh yang diberikan dalam dokumentasi untuk fungsi PHP OpenSSL_encrypt ().
Perpustakaan ini bertujuan untuk memastikan bahwa data yang dienkripsinya sama amannya dengan kunci rahasia yang digunakan untuk mengenkripsi. Langkah -langkah yang juga diambil dalam upaya untuk memastikan bahwa data yang dienkripsi kedap diri.
Perpustakaan ini tidak dapat memecahkan masalah sulit manajemen kunci.

Perpustakaan ini sedang dalam proses.
Saya berbagi kode ini dengan teman dan kolega, meminta kritik dan umpan balik sebanyak mungkin. Ketika saya merasa perpustakaan ini sama baiknya dengan yang saya bisa, saya akan memperbarui catatan status ini. Sementara itu, perubahan waktu hampir pasti dan kelemahan enkripsi sangat mungkin. Jika Anda menemukan sesuatu yang menurut Anda harus saya ketahui, beri tahu saya!
Saya ingin menjadi jelas dengan Anda bahwa perpustakaan ini besar dan kompleks dan belum banyak digunakan; Tidak diragukan lagi sarat dengan serangga halus yang belum ditemukan. Saya pikir basis kode ini memiliki potensi untuk matang menjadi alat yang solid dan andal, tetapi kita perlu melalui proses.
Harap baca bagian ini.
Ada banyak cara Anda bisa salah dengan kode crypto Anda. Perpustakaan ini ditulis sebagai upaya untuk mengurangi footgun crypto; Semoga tidak memperkenalkan apa pun!
Hal pertama yang perlu diketahui tentang crypto adalah data Anda hanya seaman kunci Anda. Ada lebih banyak yang perlu diketahui tentang manajemen kunci daripada yang mungkin saya katakan di sini (dan saya toh bukan ahli), tetapi di sini ada beberapa hal untuk dipikirkan:
Beberapa hal lain yang harus diperhatikan:
get_error() setelah enkripsi untuk memastikan itu tidak menunjukkan kesalahan).Hal lain, yang mengejutkan saya ketika saya mempelajarinya, meskipun cukup jelas setelah Anda tahu, adalah bahwa Anda tidak boleh memampatkan data Anda sebelum Anda mengenkripsi. Ini tidak selalu menjadi masalah, tetapi dalam keadaan tertentu itu bisa, jadi mungkin yang terbaik tidak pernah melakukannya.
Masalah dengan kompresi adalah bahwa jika penyerang dapat mengontrol beberapa data input mereka dapat memasukkan nilai tertentu dan kemudian jika output berkurang dalam ukuran mereka dapat mengetahui bahwa input lain juga termasuk dalam nilai tertentu. Aduh.
Basis kode ini tidak matang atau diuji dengan baik, sebelum Anda menggunakannya, Anda harus membaca semua kode untuk memastikan itu memenuhi standar kualitas Anda. Jika Anda melakukannya, saya akan senang mendengar dari Anda.
Jika Anda dapat memikirkan hal lain yang harus diketahui semua orang dan berhati -hati, beri tahu saya!
Tidak ingin RTFM ..? Dan di sinilah saya, menulis semua hal ini ... Sheesh. Setidaknya baca peringatan yang tercantum di atas.
#!/bin/bash
set -euo pipefail;
mkdir -p kickass-demo/lib
cd kickass-demo
git clone https://github.com/jj5/kickass-crypto.git lib/kickass-crypto 2>/dev/null
php lib/kickass-crypto/bin/gen-demo-config.php > config.php
cat > demo.php <<'EOF'
<?php
require_once __DIR__ . '/lib/kickass-crypto/inc/sodium.php';
require_once __DIR__ . '/config.php';
$ciphertext = kickass_round_trip()->encrypt( 'secret text' );
$plaintext = kickass_round_trip()->decrypt( $ciphertext );
echo "the secret data is: $plaintext.n";
EOF
php demo.php
Untuk elaborasi yang sedikit lebih mungkin periksa kode sampel.
Atau jika Anda ingin intinya tentang cara kerja perpustakaan ini membaca kode dalam kerangka perpustakaan atau kode lainnya.
Wah, itu dimulai cukup tetapi pada akhirnya rumit.
Saya ingin melakukan perjalanan bundar beberapa data yang relatif sensitif (nomor versi baris untuk kontrol konkurensi yang optimis) antara server saya dan kliennya dengan cara yang relatif aman, kerahasiaan, dan tamperproofing lebih disukai.
Saya telah mendengar bahwa Perpustakaan OpenSSL tersedia dalam PHP jadi saya mencari informasi tentang cara menggunakannya. Saya menemukan kode contoh dalam dokumentasi PHP untuk fungsi openssl_encrypt ().
Awalnya tidak jelas bagi saya cara menggunakan kode ini. Terutama sulit untuk mengetahui apa yang harus dilakukan dengan tiga bagian: tag otentikasi, vektor inisialisasi, dan teks sandi. Akhirnya saya tahu saya bisa menggabungkan mereka. Tetapi jika saya harus melakukan itu, saya perlu menstandarkan panjang dan penempatan mereka sehingga saya bisa mengambilnya nanti ...
... dan kemudian saya pikir akan lebih baik untuk menutupi ukuran data saya yang sebenarnya dengan memadukannya ke panjang tetap pada batas -batas tertentu, jadi saya melakukannya ...
... dan kemudian saya ingin mendukung data kaya yang menuntut beberapa bentuk serialisasi. Awalnya saya menggunakan fungsi php serialize () tetapi itu diubah kemudian menjadi json_encode ().
Kode contoh tidak menunjukkan apa pun tentang cara memutar kunci dengan cara yang didukung. Jadi saya datang dengan dua kasus penggunaan yang didukung oleh perpustakaan ini dengan pendekatan berbeda untuk manajemen kunci untuk skenario pulang-pergi dan at-rest. Perpustakaan ini memungkinkan Anda memutar kunci baru sambil mempertahankan dukungan untuk kunci yang lebih lama, karena Anda mungkin tidak akan melakukannya.
Kemudian saya melapisi pendekatan yang cermat untuk penanganan pengecualian dan pelaporan kesalahan, beberapa pengujian dan validasi unit, mitigasi serangan waktu, pelacak layanan, demonstrasi penggunaan, batas ukuran data, inisialisasi frasa sandi, skrip pembuatan utama, telemetri, dan hal -hal seperti itu.
Pada dasarnya seluruh perpustakaan ini adalah semua yang saya rasa harus saya lakukan sehingga saya benar-benar dapat menggunakan implementasi perpustakaan PHP OpenSSL bawaan.
Dan kemudian ... orang -orang mulai memberi tahu saya tentang perpustakaan natrium, dan menyarankan agar saya menggunakannya. Karena saya sudah melakukan banyak pekerjaan untuk manajemen kunci dan serialisasi input dan pemformatan dan pengkodean pesan dan sebagainya saya pikir saya bisa menggunakan kembali semua itu dan menyediakan pembungkus di sekitar natrium juga. Jadi itulah yang saya lakukan.
Sekarang jika Anda menggunakan perpustakaan ini, Anda dapat memutuskan apakah Anda ingin menggunakan implementasi natrium atau implementasi OpenSSL. Karena dua implementasi dapat dengan senang hati hidup berdampingan, Anda juga dapat menulis kode untuk berpindah dari satu ke yang lain, jika Anda sangat menginginkannya. Implementasi tidak pernah berbagi konfigurasi utama atau format data, mereka sepenuhnya terpisah. ;
Saya tidak menganggap perpustakaan ini menggulung crypto saya sendiri , melainkan saya menganggapnya mencari cara untuk benar -benar menggunakan natrium dan openssl . Jika saya membuat kesalahan, jelas atau sebaliknya, saya akan sangat menghargai mendengarnya.
Dengan asumsi saya ingat untuk memperbaruinya dari waktu ke waktu, ada sistem demo di sini:
Fasilitas demo hanya menunjukkan cara melakukan perjalanan dienkripsi antara klien dan server menggunakan HTML dan HTTP.
Kode demo tersedia di perpustakaan ini di src/ demo/ direktori jika Anda ingin meng -hostnya sendiri.
Dengan asumsi saya ingat untuk memperbaruinya dari waktu ke waktu, dokumen PHP ada di sini:
Seperti disebutkan di atas, Anda dapat memeriksa kode dari git dengan perintah seperti ini:
git clone https://github.com/jj5/kickass-crypto.git
Kode ini belum dirilis, tidak ada versi yang stabil.
Jika Anda ingin memasukkan pustaka klien untuk digunakan dalam aplikasi Anda termasuk file inc/nodium.php atau inc/openssl.php yang akan menangani pemuatan yang lainnya; Gunakan sesuatu seperti ini:
require_once __DIR__ . '/lib/kickass-crypto/inc/sodium.php';
Setelah memuat perpustakaan ini, Anda biasanya akan mengakses melalui locator layanan kickass_round_trip() atau kickass_at_rest() yang didokumentasikan di bawah ini, sesuatu seperti ini:
$ciphertext = kickass_round_trip()->encrypt( 'secret text' );
$plaintext = kickass_round_trip()->decrypt( $ciphertext );
echo "the secret data is: $plaintext.n";
Butuh banyak pekerjaan untuk membuat hal -hal sesederhana itu!
Jika Anda ingin meng -host kode demo, Anda perlu meng -host file di SRC/ Demo/ dan menyertakan file config.php yang valid di direktori basis proyek (itulah direktori yang menyertakan file readme.md ini). Untuk tujuan demonstrasi file config.php yang valid hanya perlu menentukan string konstan untuk CONFIG_SODIUM_SECRET_CURR , tetapi perlu string yang panjang dan acak, Anda dapat menghasilkan string yang sesuai dengan:
php bin/gen-key.php
Atau Anda bisa menghasilkan seluruh file config.php demo dengan:
php bin/gen-demo-config.php > config.php
Berikut adalah beberapa catatan tentang file perangkat lunak dan baris kode.
Total Number of Files = 128
Total Number of Source Code Files = 128
| Direktori | File | Berdasarkan bahasa |
|---|---|---|
| tes | 63 | PHP = 59, SH = 4 |
| kode | 35 | PHP = 35 |
| tempat sampah | 22 | PHP = 13, SH = 9 |
| inc | 7 | php = 7 |
| demo | 1 | php = 1 |
| Bahasa | File | Persentase |
|---|---|---|
| php | 115 | (89,84%) |
| sh | 13 | (10,16%) |
Total Physical Source Lines of Code (SLOC) = 9,210
Development Effort Estimate, Person-Years (Person-Months) = 2.06 (24.70)
(Basic COCOMO model, Person-Months = 2.4 * (KSLOC**1.05))
Schedule Estimate, Years (Months) = 0.70 (8.46)
(Basic COCOMO model, Months = 2.5 * (person-months**0.38))
Estimated Average Number of Developers (Effort/Schedule) = 2.92
Total Estimated Cost to Develop = $ 278,044
(average salary = $56,286/year, overhead = 2.40).
| Direktori | Sloc | Berdasarkan bahasa |
|---|---|---|
| kode | 5.136 | PHP = 5136 |
| tes | 3.363 | PHP = 3193, SH = 170 |
| tempat sampah | 603 | PHP = 423, SH = 180 |
| demo | 71 | PHP = 71 |
| inc | 37 | PHP = 37 |
| Bahasa | Sloc | Persentase |
|---|---|---|
| php | 8.860 | (96,20%) |
| sh | 350 | (3,80%) |
Kode ini harus bekerja pada Php 7.4 atau lebih besar. Jika Anda mencoba menjalankan kode ini pada versi PHP yang lebih lama, ia akan mencoba mencatat pesan kesalahan dan kemudian keluar dari proses Anda.
Kode ini akan memeriksa untuk memastikan itu berjalan pada platform 64-bit. Jika tidak, itu akan mengeluh dan keluar.
Jika Anda memuat modul natrium, perpustakaan akan memastikan bahwa pustaka natrium sebenarnya tersedia. Jika tidak, prosesnya akan mengeluh dan keluar.
Jika Anda memuat modul OpenSSL, perpustakaan akan memastikan bahwa pustaka OpenSSL sebenarnya tersedia. Jika tidak, prosesnya akan mengeluh dan keluar.
Saya percaya kode ini harus berjalan pada sistem operasi apa pun, tetapi saya hanya mengujinya di Linux. Jika Anda telah sukses di macOS atau Windows, saya akan senang mendengarnya.
Skrip shell ditulis untuk bash. Jika Anda tidak memiliki bash, Anda mungkin perlu port.
Kode ini mendukung dua kasus penggunaan khusus:
Kunci dikelola secara terpisah dan berbeda untuk setiap kasus penggunaan.
Rincian bagaimana setiap kasus penggunaan didukung didokumentasikan di bawah ini.
Menggunakan perpustakaan ini untuk enkripsi AT-REST umumnya merupakan risiko yang lebih besar dan komitmen yang lebih besar daripada menggunakannya hanya untuk enkripsi pulang-pergi. Jika Anda kehilangan kunci enkripsi pulang-pergi Anda atau dipaksa untuk memutarnya segera, kemungkinan akan lebih sedikit masalah daripada jika sesuatu yang serupa terjadi dengan kunci at-rest Anda.
Kasus penggunaan utama yang dikembangkan perpustakaan ini adalah untuk mendukung tripping bundar beberapa kilobyte data yang berisi angka versi baris kritis misi untuk kontrol konkurensi yang optimis. Dibandingkan dengan alternatif (bukan mengenkripsi atau mengutak -atik data kontrol konkurensi optimis) penggunaan perpustakaan ini merupakan peningkatan. Apakah itu benar -benar cocok di aplikasi lain adalah pertanyaan terbuka, saya tidak yakin. Tentu saja Anda tidak boleh menggunakan perpustakaan ini jika tidak memberikan tingkat keamanan yang Anda butuhkan.
Cara yang disukai dan didukung untuk mencalonkan rahasia dalam file konfigurasi adalah sebagai konstanta menggunakan fungsi PHP Define (). Masalah dengan menggunakan bidang kelas/instance atau variabel global adalah bahwa nilai -nilai tersebut dapat dengan mudah bocor ke dalam kode debug dan logging, ini lebih kecil kemungkinannya (meskipun masih mungkin) untuk konstanta. Demikian pula jika Anda perlu cache data global/statis (seperti membaca dari file config) cara terbaik untuk melakukannya adalah dengan variabel statis lokal dalam suatu fungsi, jika memungkinkan, karena menggunakan bidang instance, bidang kelas, atau global dapat lebih mudah menyebabkan kebocoran rahasia.
Untuk memberi Anda contoh, mari kita buat file uji yang disebut double-define.php seperti ini:
<?php
define( 'TEST', 123 );
define( 'TEST', 456 );
Kemudian ketika kita menjalankan kode, sesuatu seperti ini terjadi:
$ php double-define.php
PHP Warning: Constant TEST already defined in ./double-define.php on line 4
PHP Stack trace:
PHP 1. {main}() ./double-define.php:0
PHP 2. define($constant_name = 'TEST', $value = 456) ./double-define.php:4
Jika nilai konstan itu berisi kunci rahasia Anda maka Anda baru saja mengalami hari yang sangat buruk.
Cara teraman untuk mendefinisikan konstan dalam PHP adalah dengan memeriksa apakah itu belum ditentukan terlebih dahulu, karena berusaha untuk mendefinisikan konstanta yang sudah ditentukan akan menghasilkan kesalahan. Jika Anda menemukan konstanta yang sudah ditentukan, Anda dapat membatalkan dengan pesan kesalahan (jika Anda tidak memberikan terlalu banyak detail karena web publik mungkin melihatnya) atau hanya menjaga nilai yang ada dan tidak mencoba untuk mendefinisikannya kembali. Generator file konfigurasi Bin/Gen-Demo-Demo-Config.php mengambil pendekatan pertama dan memanggil fungsi php die() jika duplikat terdeteksi. Anda dapat melihat apa yang terjadi dengan memasukkan file config.php yang dihasilkan dua kali, sebagai:
require __DIR__ . '/config.php';
require __DIR__ . '/config.php';
Anda dapat menemukan contoh dari apa yang terjadi jika Anda menggandakan config.php di config-die.php.
Akibatnya, seperti halnya sebagian besar file sumber PHP, yang terbaik adalah menggunakan require_once saat memasukkan file config.php :
require_once __DIR__ . '/config.php';
Ketika saya menyebutkan hal -hal yang rahasia, saya memastikan namanya berisi string "pass" (seperti dalam "kata sandi", "passwd", dan "frasa pasal", atau bahkan, pada peregangan, "paspor") atau "rahasia". Dalam fasilitas logging tujuan umum saya (yang tidak termasuk dalam perpustakaan ini) saya scrub dan redact apa pun dengan nama yang cocok (case-non-sensitive) sebelum login data diagnostik. Saya mendorong Anda untuk mengadopsi praktik ini.
Di pustaka ini jika variabel atau konstan mungkin berisi data sensitif itu akan dinamai dengan "pass" atau "rahasia" sebagai substring dalam nama.
Jangan menulis data sensitif ke dalam log.
Letakkan 'pass' atau 'rahasia' atas nama variabel sensitif, bidang, atau konstanta.
Di sini saya menjelaskan apa arti istilah yang terdengar serupa ini dalam konteks perpustakaan ini.
Jika Anda menggunakan modul default, format data adalah "Ka0" untuk modul OpenSSL atau "KAS0" untuk modul natrium.
Jika Anda mewarisi kerangka dasar dan mendefinisikan modul crypto Anda sendiri, format data default adalah "XKA0" untuk modul berdasarkan implementasi OpenSSL atau "XKAS0" untuk modul berdasarkan implementasi natrium yang tidak dapat do_get_const_data_format() dapat diketahui seperti itu, Anda tidak dapat diketahui seperti itu, Anda dapat diketahui seperti itu, Anda dapat diketahui seperti itu, Anda dapat diketahui seperti itu, Anda dapat diketahui seperti itu, Anda dapat diketahui seperti itu, Anda dapat diketahui seperti itu, Implementasi.
Anda perlu menggunakan modul yang tepat untuk format data agar berhasil mendekripsi ciphertext.
Pengkodean data adalah JSON, serialisasi PHP, atau teks. Dengan asumsi Anda memiliki modul yang tepat untuk format data (di atas), dan dengan peringatan yang dibahas di bawah ini, Anda dapat mendekripsi apa pun terlepas dari pengkodean data yang digunakan. Enkripsi akan dilakukan dengan menggunakan pengkodean data yang dikonfigurasi, lihat config_encryption_data_encoding, itu bisa menjadi salah satu dari:
Perhatikan bahwa Anda tidak akan dapat menggunakan pengkodean PHP kecuali Anda juga mendefinisikan config_encryption_phps_enable, ini karena deserialisasi PHP mungkin tidak aman sehingga dinonaktifkan secara default. Sejujurnya ini sedikit bergandengan tangan. Saya baru saja mendengar desas -desus bahwa PHP unserialize() dapat menyebabkan injeksi kode tetapi saya tidak yakin apakah itu benar atau apa artinya itu. Saya menerapkan serialisasi dan deserialisasi PHP dan memberinya sedikit tes tetapi saya tidak tahu apakah itu benar -benar tidak aman atau tidak. Saya cukup yakin bahwa pengkodean data JSON dan teks harus aman.
Selain mewarisi dari KickassCrypto dan menimpa fungsionalitas khusus, banyak konfigurasi tersedia melalui konstanta konfigurasi. Cari CONFIG_SODIUM untuk menemukan apa yang tersedia untuk natrium dan CONFIG_OPENSSL untuk menemukan apa yang tersedia untuk openssl.
Harap disarankan bahwa saat ini kode ini dikonfigurasi langsung di file config.php .
Di masa depan file config.php akan mencakup file konfigurasi yang dikelola secara terpisah, menjadi:
Akan ada skrip manajemen untuk kunci putar dan penyediaan secara otomatis dalam file -file ini.
Pengguna Linux yang berpengalaman tahu bahwa Anda tidak mengedit /etc/sudoers secara langsung, Anda mengeditnya dengan visudo sehingga Anda dapat memverifikasi bahwa Anda belum secara tidak sengaja memperkenalkan kesalahan sintaksis dan menyemprot sistem Anda.
Saya bermaksud memberikan skrip serupa untuk mengedit dan mengelola config.php dan file konfigurasi lainnya. Jadi siaga untuk pembaruan itu. Sementara itu ... berhati -hatilah .
Satu hal yang Anda harus sangat berhati -hati yang tidak Anda lakukan adalah mengelola kunci Anda selain file PHP dengan ekstensi file ".php". Jika Anda memasukkan kunci Anda ke dalam file ".ini" atau semacamnya , mereka mungkin disajikan sebagai teks biasa oleh server web Anda . Jadi jangan lakukan itu. Juga berhati -hatilah untuk tidak memasukkan kesalahan sintaks ke dalam file konfigurasi Anda atau file sumber lain yang berjalan dalam produksi karena detail mungkin bocor dengan pesan kesalahan yang dihasilkan potensial.
Seperti disebutkan di bagian sebelumnya sejumlah besar konfigurasi disediakan oleh dukungan untuk konstanta konfigurasi yang disebutkan.
Selain konstanta konfigurasi, ada banyak hal yang dapat Anda lakukan jika Anda mewarisi dari kelas dasar KickassCrypto dan mengganti metodenya.
Sebagai alternatif untuk konstanta konfigurasi (yang hanya dapat didefinisikan sekali per proses dan setelah itu tidak dapat diubah) ada metode contoh sebagai get_config_...() untuk opsi konfigurasi dan get_const_...() untuk evaluasi konstan. Konstanta dan opsi konfigurasi yang paling penting dibaca secara tidak langsung melalui aksesor ini sehingga Anda harus dapat mengesampingkannya dengan andal.
Sebagian besar panggilan ke fungsi built-in php dilakukan oleh pembungkus tipis melalui fungsi yang dilindungi di KickassCrypto . Ini didefinisikan dalam sifat KICKASS_WRAPPER_PHP . Tidak langsung ini memungkinkan doa fungsi PHP tertentu dicegat dan berpotensi dimodifikasi. Ini telah dilakukan terutama untuk mendukung injeksi kesalahan selama pengujian unit, tetapi Anda dapat menggunakan untuk tujuan lain untuk mengubah keterangan implementasi.
Hal -hal yang dianggap sensitif di KickassCrypto didefinisikan sebagai pribadi atau final . Jika tidak pribadi dan itu bukan permainan yang adil untuk mengesampingkan (kecuali saya telah melakukan kesalahan). Khususnya metode instan yang dimulai dengan do_ secara khusus dibuat untuk diganti atau dicegat oleh pelaksana.
Perpustakaan ini menyediakan dua fungsi pelacak layanan yang mengelola contoh perpustakaan crypto masing -masing, itu adalah:
kickass_round_trip()kickass_at_rest()Anda dapat mengganti contoh layanan yang disediakan oleh fungsi Layanan Locator dengan memanggil fungsi dan meneruskan instance baru sebagai parameter tunggal, seperti ini:
class MyKickassCrypto extends KickassCryptoKickassCrypto {
protected function do_is_valid_config( &$problem = null ) { return TODO; }
protected function do_get_passphrase_list() { return TODO; }
// ... other function overrides ...
}
kickass_round_trip( new MyKickassCrypto );
Idealnya perpustakaan ini akan memenuhi persyaratan Anda di luar kotak (atau dengan konfigurasi tertentu) dan Anda tidak perlu mengganti instance yang disediakan oleh Layanan Layanan secara default.
Locator layanan akan membuat instance default baru untuk Anda pada panggilan pertama ke Locator Layanan jika belum memiliki instance. Apakah implementasi default adalah modul natrium atau modul OpenSSL tergantung pada urutan yang Anda sertakan file inc/sodium.php dan inc/openssl.php ; Jika Anda memasukkan seluruh perpustakaan dengan inc/library.php modul natrium akan diutamakan.
Terlepas dari apakah Anda memuat locator layanan untuk modul natrium atau modul OpenSSL, Anda akan dapat mengganti instance default dengan menghubungi locator layanan dengan instance baru sebagai argumen.
Proses enkripsi secara kasar:
Perhatikan bahwa pustaka natrium menggunakan nonce bukan vektor inisialisasi (untuk efek yang sama) dan natrium menangani label otentikasi sendiri.
Ketika perpustakaan ini mengkodekan ciphertext-nya, itu mencakup awalan format data dari "KAS0/" untuk implementasi natrium dan "Ka0/" untuk implementasi OpenSSL.
Zero ("0") dalam awalan format data adalah untuk versi nol , yang dimaksudkan untuk menyiratkan bahwa antarmuka tidak stabil dan dapat berubah .
Versi masa depan perpustakaan ini dapat menerapkan awalan format data baru untuk format data yang stabil.
Ketika perpustakaan ini mendekode ciphertext-nya, ia memverifikasi awalan format data. Saat ini hanya "Kas0/" atau "Ka0/" didukung.
Format data nol versi, yang disebutkan di atas, saat ini menyiratkan hal berikut:
Setelah pengkodean data (JSON secara default, dibahas di bagian berikut) Padding dilakukan dan panjang data diawali. Sebelum enkripsi pesan diformat, seperti ini:
$message = $encoded_data_length . '|json|' . $encoded_data . $this->get_padding( $pad_length );
Panjang data JSON diformat sebagai nilai heksadesimal 8-karakter. Ukuran 8 karakter konstan dan tidak bervariasi tergantung pada besarnya panjang data JSON.
Alasan untuk bantalan adalah untuk mengaburkan ukuran data aktual. Padding dilakukan dalam hingga 4 batas kib (2 12 byte), yang kami sebut potongan. Ukuran chunk dapat dikonfigurasi dan default dapat berubah di masa depan.
Maka jika kita mengenkripsi dengan natrium, pesan dienkripsi dengan sodium_crypto_secretbox() dan kemudian nonce dan ciphertext digabungkan bersama, seperti ini:
$nonce . $ciphertext
Kalau tidak, jika kita mengenkripsi dengan openssl, pesan dienkripsi dengan AES-256-GCM dan vektor inisialisasi, ciphertext, dan tag otentikasi digabungkan bersama, seperti ini:
$iv . $ciphertext . $tag
Maka semuanya basis64 yang dikodekan dengan fungsi php base64_encode () dan awalan format data ditambahkan.
Untuk natrium yang dilakukan seperti ini:
"KAS0/" . base64_encode( $nonce . $ciphertext )
Dan untuk openssl itu dilakukan seperti ini:
"KA0/" . base64_encode( $iv . $ciphertext . $tag )
Proses dekripsi mengharapkan untuk menemukan 24 byte nonce dan ciphertext untuk format data "KAS0" dan vektor inisialisasi 12 byte, ciphertext, dan tag otentikasi 16 byte untuk format data KA0.
Setelah mendekripsi ciphertext, perpustakaan mengharapkan untuk menemukan ukuran data JSON sebagai string ASCII yang mewakili nilai hex hex 8 karakter, diikuti oleh karakter pipa tunggal, diikuti oleh indikator pengkodean data empat karakter ('JSON' atau 'PHP'), diikuti oleh karakter pipa tunggal, diikuti oleh JSON (atau data serial php), dan PHP. Perpustakaan kemudian dapat mengekstraksi data JSON/serial dari bantalannya dan mengurus decoding lainnya.
Sebelum data input enkripsi dikodekan sebagai JSON menggunakan fungsi PHP JSON_ENCODE (). Awalnya perpustakaan ini menggunakan fungsi PHP Serialize () tetapi tampaknya yang dapat menyebabkan beberapa skenario pengeksekusi kode (saya tidak yakin pada detailnya) sehingga diputuskan bahwa pengkodean JSON lebih aman. Jadi, sekarang, kami menggunakan encoding JSON sebagai gantinya.
Penggunaan JSON sebagai format pengkodean data memiliki beberapa implikasi kecil mengenai nilai -nilai yang dapat kami dukung. Terutama kita tidak dapat mengkodekan instance objek yang nantinya dapat diterjemahkan kembali ke instance objek (jika objek menerapkan antarmuka jSonserializable, mereka dapat diserialisasi sebagai data, tetapi itu hanya akan diterjemahkan kembali ke array PHP, bukan objek PHP dari mana mereka datang); beberapa nilai titik mengambang aneh tidak dapat diwakili (yaitu nan, pos inf, neg info, dan neg nol); dan string biner tidak dapat diwakili dalam JSON.
Secara default opsi ini digunakan untuk pengkodean JSON:
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
Tetapi opsi -opsi ini tidak akan mempengaruhi kemampuan implementasi untuk memecahkan kode JSON. Implementasi dapat menyempurnakan pengkodean dan decoding JSON jika perlu dengan mengesampingkan metode data_encode () dan data_decode (). Atau Anda dapat mencalonkan opsi pengkodean dan decoding JSON di file config.php Anda dengan CONFIG_ENCRYPTION_JSON_ENCODE_OPTIONS dan CONFIG_ENCRYPTION_JSON_DECODE_OPTIONS , misalnya: misalnya: misalnya: misalnya: misalnya:
define( 'CONFIG_ENCRYPTION_JSON_ENCODE_OPTIONS', JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
define( 'CONFIG_ENCRYPTION_JSON_ENCODE_OPTIONS', JSON_THROW_ON_ERROR );
Perpustakaan ini harus bekerja terlepas dari apakah JSON_THROW_ON_ERROR ditentukan atau tidak.
Jika Anda menentukan JSON_PARTIAL_OUTPUT_ON_ERROR dalam opsi pengkodean JSON Anda data Anda mungkin diam -diam menjadi tidak valid, jadi lakukan itu dengan risiko Anda sendiri. Mungkin kontra-intuisi saya telah menemukan bahwa mengaktifkan JSON_PARTIAL_OUTPUT_ON_ERROR adalah strategi paling buruk karena setidaknya dalam hal ini Anda mendapatkan sesuatu . Jika Anda tidak mengaktifkan JSON_PARTIAL_OUTPUT_ON_ERROR jika ada bagian dari input Anda yang tidak dapat dikodekan (seperti ketika Anda memiliki string biner yang tidak ada dalam penyandian yang valid seperti UTF-8) maka seluruh data dihapus. Dengan JSON_PARTIAL_OUTPUT_ON_ERROR Hanya bagian yang tidak dapat diperbaiki yang dihilangkan. Saat ini JSON_PARTIAL_OUTPUT_ON_ERROR tidak ditentukan secara otomatis, tetapi ini adalah sesuatu yang mungkin saya kunjungi kembali di masa depan.
Jika Anda menggunakan salah satu opsi pengkodean/decoding JSON ini, Anda mungkin akan memiliki waktu yang buruk:
JSON_NUMERIC_CHECKJSON_INVALID_UTF8_IGNOREJSON_INVALID_UTF8_SUBSTITUTE Ketika perpustakaan ini mengenkripsi datanya, ia membanting output hingga ukuran chunk yang dapat dikonfigurasi.
Konstanta konfigurasi untuk ukuran chunk adalah CONFIG_ENCRYPTION_CHUNK_SIZE .
Ukuran chunk default adalah 4.096 (2 12 ).
Jika Anda ingin meningkatkan ukuran chunk menjadi 8.192, Anda bisa melakukannya di file config.php Anda seperti ini:
define( 'CONFIG_ENCRYPTION_CHUNK_SIZE', 8912 );
Anda dapat mengubah ukuran chunk yang ditentukan dan akan mulai berlaku untuk data baru, dan data lama yang dienkripsi dengan ukuran chunk yang berbeda masih dapat didekripsi.
Selama batas ukuran data diamati (ini dibahas selanjutnya), perpustakaan ini dapat mengenkripsi apa pun yang dapat dikodekan sebagai JSON oleh PHP.
Ini termasuk berbagai hal, seperti:
Hal -hal yang tidak dapat didukung dengan JSON:
Perhatikan bahwa nilai boolean false tidak dapat dienkripsi. Itu bukan karena kami tidak bisa mengenkripsi, itu karena kami mengembalikannya ketika dekripsi gagal. Jadi kami menolak untuk mengenkripsi false sehingga tidak dapat disamakan dengan kesalahan setelah dekripsi.
Jika Anda perlu mengenkripsi nilai boolean false, pertimbangkan untuk memasukkannya ke dalam array, seperti ini:
$input = [ 'value' => false ];
Atau pengkodean sebagai JSON, seperti ini:
$input = json_encode( false );
Jika Anda melakukan salah satu dari hal -hal itu, Anda akan dapat mengenkripsi nilai Anda.
Perlu ditunjukkan bahwa dalam "string" PHP pada dasarnya adalah array byte, yang berarti mereka dapat berisi data "biner" pada dasarnya. Namun data biner seperti itu tidak dapat direpresentasikan sebagai JSON. Jika Anda perlu menangani data biner dengan cara terbaik mungkin adalah dengan mengkodekannya sebagai base64 dengan base64_encode () atau hexadecimal dengan bin2hex () dan kemudian mengenkripsi itu.
Di masa depan kemampuan untuk bekerja dengan data yang tidak selalu dikodekan JSON mungkin ditambahkan ke perpustakaan ini. Beri tahu saya jika itu fitur yang Anda mau miliki.
Catatan: Menggunakan serialisasi PHP alih -alih encoding JSON sekarang menjadi opsi; Dokumentasi ini perlu diperbarui untuk menjelaskan cara kerjanya dan cara menggunakannya. Keuntungan dari serialisasi PHP adalah mendukung lebih banyak tipe dan format data daripada JSON.
Setelah data dikodekan sebagai JSON terbatas pada panjang maksimum yang dapat dikonfigurasi.
Konstanta konfigurasi untuk panjang pengkodean JSON maksimum adalah CONFIG_ENCRYPTION_DATA_LENGTH_MAX .
Batas pengkodean data default adalah 67.108.864 (2^ 26 ) byte, yang kira -kira 67 MB atau 64 MIB.
Dimungkinkan untuk mengonfigurasi batas pengkodean data ini, jika Anda perlu membuatnya lebih besar atau lebih kecil. Ketahuilah bahwa jika Anda membuat batas terlalu besar, Anda akan berakhir dengan masalah memori dan proses Anda mungkin akan diakhiri.
Jika Anda ingin mengurangi batas pengkodean data, Anda dapat melakukannya di file config.php Anda seperti ini:
define( 'CONFIG_ENCRYPTION_DATA_LENGTH_MAX', pow( 2, 25 ) );
Perpustakaan ini tidak memampatkan data input, karena kompresi dapat memperkenalkan kelemahan kriptografi, seperti dalam serangan kejahatan SSL/TLS.
Masalahnya adalah bahwa jika penyerang dapat memodifikasi beberapa teks biasa, mereka dapat mengetahui apakah data yang mereka masukkan ada di bagian lain dari teks biasa, karena jika mereka memberi nilai dan hasilnya lebih kecil itu karena ada di bagian teks biasa yang tidak mereka ketahui, tetapi lakukan sekarang!
Sangat penting bahwa Anda tidak mengompres data yang dapat disediakan oleh penyerang dengan data lain yang rahasia. Yang terbaik adalah tidak mengompres sama sekali.
Jika kesalahan ditemui selama enkripsi atau dekripsi, penundaan antara 1 milidetik (1 ms) dan 10 detik (10 detik) diperkenalkan. Ini adalah mitigasi terhadap potensi serangan waktu. See s2n and Lucky 13 for discussion.
Note that avoiding timing attacks is hard. A malicious guest on your VPS host (or a malicious person listening to your server's fans! ?) could figure out that your process is sleeping rather than doing actual work.
This library includes a method called delay() , and this method is called automatically on the first instance of an error. The delay() method does what is says on the tin: it injects a random delay into the process. The delay() method is public and you can call it yourself if you feel the need. Each time delay() is called it will sleep for a random amount of time between 1 millisecond and 10 seconds.
The programmer using this library has the opportunity to override the do_delay() method and provide their own delay logic.
If that do_delay() override throws an exception it will be handled and an emergency delay will be injected.
If you do override do_delay() but don't actually delay for at least the minimum duration (which is 1 ms) then the library will inject the emergency delay.
The main reason for allowing the implementer to customize the delay logic is so that unit tests can delay for a minimum amount of time. Ordinarily there shouldn't be any reason to meddle with the delay logic and it might be less safe to do so.
When an instance of one of of the following is created the configuration settings are validated.
KickassSodiumRoundTripKickassSodiumAtRestKickassOpenSSLRoundTripKickassOpenSSLAtRestIf the configuration settings are not valid the constructor will throw an exception. If the constructor succeeds then encryption and decryption later on should also (usually) succeed. If there are any configuration problems that will mean encryption or decryption won't be able to succeed (such as secret keys not having been provided) the constructor should throw.
This library defines its own exception class called KickassException . This works like a normal Exception except that it adds a method getData() which can return any data associated with the exception. A KickassException doesn't always have associated data.
Of course not all problems will be able to be diagnosed in advance. If the library can't complete an encryption or decryption operation after a successful construction it will signal the error by returning the boolean value false. Returning false on error is a PHP idiom, and we use this idiom rather than raising an exception to limit the possibility of an exception being thrown while an encryption secret or passphrase is on the call stack.
The problem with having sensitive data on the call stack when an exception is raised is that the data can be copied into stack traces, which can get saved, serialized, displayed to users, logged, etc. We don't want that so we try very hard not to raise exceptions while sensitive data might be on the stack.
If false is returned on error, one or more error messages will be added to an internal list of errors. The caller can get the latest error by calling the method get_error . If you want the full list of errors, call get_error_list .
If there were any errors registered by the OpenSSL library functions (which the OpenSSL module calls to do the heavy lifting), then the last such error is available if you call the get_openssl_error() . You can clear the current error list (and OpenSSL error message) by calling the method clear_error() .
For the PHP Sodium implementation the function we use is sodium_crypto_secretbox(). That's XSalsa20 stream cipher encryption with Poly1305 MAC authentication and integrity checking.
For the PHP OpenSSL implementation the cipher suite we use is AES-256-GCM. That's Advanced Encryption Standard encryption with Galois/Counter Mode authentication and integrity checking.
Secret keys are the secret values you keep in your config.php file which will be processed and turned into passphrases for use by the Sodium and OpenSSL library functions. This library automatically handles converting secret keys into passphrases so your only responsibility is to nominate the secret keys.
The secret keys used vary based on the use case and the module. There are two default use cases, known as round-trip and at-rest.
The "256" in AES-256-GCM means that this cipher suite expects 256-bit (32 byte) passphrases. The Sodium library sodium_crypto_secretbox() function also expects a 256-bit (32 byte) passphrase.
We use a hash algorithm to convert our secret keys into 256-bit binary strings which can be used as the passphrases the cipher algorithms expect.
The minimum secret key length required is 88 bytes. When these keys are generated by this library they are generated with 66 bytes of random data which is then base64 encoded.
The secret key hashing algorithm we use is SHA512/256. That's 256-bits worth of data taken from the SHA512 hash of the secret key. When this hash code is applied with raw binary output from an 88 byte base64 encoded input you should be getting about 32 bytes of randomness for your keys.
The Sodium library expects to be provided with a nonce, in lieu of an initialization vector.
To understand what problem the nonce mitigates, think about what would happen if you were encrypting people's birthday. If you had two users with the same birthday and you encrypted those birthdays with the same key, then both users would have the same ciphertext for their birthdays. When this happens you can see who has the same birthday, even when you might not know exactly when it is. The initialization vector avoids this potential problem.
Our AES-256-GCM cipher suite supports the use of a 12 byte initialization vector, which we provide. The initialization vector ensures that even if you encrypt the same values with the same passphrase the resultant ciphertext still varies.
This mitigates the same problem as the Sodium nonce.
Our AES-256-GCM cipher suite supports the validation of a 16 byte authentication tag.
The "GCM" in AES-256-GCM stands for Galois/Counter Mode. The GCM is a Message Authentication Code (MAC) similar to a Hash-based Message Authentication Code (HMAC) which you may have heard of before. The goal of the GCM authentication tag is to make your encrypted data tamperproof.
The Sodium library also uses an authentication tag but it takes care of that by itself, it's not something we have to manage. When you parse_binary() in the Sodium module the tag is set to false.
This library requires secure random data inputs for various purposes:
There are two main options for generating suitable random data in PHP, those are:
Both are reasonable choices but this library uses random_bytes().
If the random_bytes() function is unable to generate secure random data it will throw an exception. See the documentation for details.
We also use the PHP random_int() function to generate a random delay for use in timing attack mitigation.
The round-trip use case is for when you want to send data to the client in hidden HTML form <input> elements and have it POSTed back later.
This use case is supported with two types of secret key.
The first key is called the current key and it is required.
The second key is called the previous key and it is optional.
Data is always encrypted with the current key.
Data is decrypted with the current key, and if that fails it is decrypted with the previous key. If decryption with the previous key also fails then the data cannot be decrypted, in that case the boolean value false will be returned to signal the error.
When you rotate your round-trip secret keys you copy the current key into the previous key, replacing the old previous key, and then you generate a new current key.
The config setting for the current key for the Sodium module is: CONFIG_SODIUM_SECRET_CURR .
The config setting for the current key for the OpenSSL module is: CONFIG_OPENSSL_SECRET_CURR .
The config setting for the previous key for the Sodium module is: CONFIG_SODIUM_SECRET_PREV .
The config setting for the previous key for the OpenSSL module is: CONFIG_OPENSSL_SECRET_PREV .
To encrypt round-trip data:
$ciphertext = kickass_round_trip()->encrypt( 'secret data' );
To decrypt round-trip data:
$plaintext = kickass_round_trip()->decrypt( $ciphertext );
The at-rest use case if for when you want to encrypt data for storage in a database or elsewhere.
This use case is supported with an arbitrarily long list of secret keys.
The list must include at least one value. The first value in the list is used for encryption. For decryption each secret key in the list is tried until one is found that works. If none work the data cannot be decrypted and the boolean value false is returned to signal the error.
When you rotate your at-rest secret keys you add a new master key as the first item in the list. You need to keep at least one extra key, and you can keep as many in addition to that as suits your purposes.
After you rotate your at-rest secret keys you should consider re-encrypting all your existing at-rest data so that it is using the latest key. After you have re-encrypted your at-rest data, you can remove the older key.
The config setting for the key list for the Sodium module is: CONFIG_SODIUM_SECRET_LIST .
The config setting for the key list for the OpenSSL module is: CONFIG_OPENSSL_SECRET_LIST .
Please be aware: if you restore an old backup of your database, you will also need to restore your old keys.
Be very careful that you don't lose your at-rest secret keys. If you lose these keys you won't be able to decrypt your at-rest data.
To encrypt at-rest data:
$ciphertext = kickass_at_rest()->encrypt( 'secret data' );
To decrypt at-test data:
$plaintext = kickass_at_rest()->decrypt( $ciphertext );
It has been noted that key management is the hardest part of cybersecurity. This library can't help you with that.
Your encrypted data is only as secure as the secret keys.
If someone gets a copy of your secret keys, they will be able to decrypt your data.
If someone gets a copy of your encrypted data now, they can keep it and decrypt it if they get a copy of your secret keys in the future. So your keys don't have to be only secret now, but they have to be secret for all time.
If you lose your secret keys, you won't be able to decrypt your data.
Your round-trip data is probably less essential than your at-rest data.
It's a very good idea to make sure you have backups of the secret keys for your essential round-trip or at-rest data. You can consider:
When doing key management it is important to make sure your config files are edited in a secure way. A syntax error in a config file could lead to a secret key being exposed to the public web. If this happened you would have to rotate all of your keys immediately and then destroy the old compromised keys, even then it might be too late .
It would be a good idea to stand ready to do a key rotation in an automated and tested fashion immediately in case of emergency.
When you rotate your round-trip and at-rest keys you need to make sure they are synchronized across all of your web servers.
I intend to implement some facilities to help with key deployment and config file editing but those facilities are not done yet.
This library supports encrypted data at-rest, and encrypted data round-trips. Another consideration is data in motion. Data in motion is also sometimes called data in transit.
Data is in motion when it moves between your web servers and your database server. Data is also in motion when it moves between your web servers and the clients that access them. You should use asymmetric encryption for your data in motion. Use SSL encryption support when you connect to your database, and use HTTPS for your web clients.
This library is a server-side component. We don't support encrypting data client-side in web browsers.
This library collects some basic telemetry:
Call KickassCrypto::GetTelemetry() to get the telemetry and KickassCrypto::ReportTelemetry() to report it.
The unit tests are in the src/test/ directory, numbered sequentially.
There's some test runners in bin/dev/, as you can see. Read the scripts for the gory details but in brief:
There are also some silly tests, but we won't talk about those. They are not ordinarily run. And they're silly.
If you want to add a normal/fast test create the unit test directory as src/test/test-XXX , then add either fast.php or fast.sh . If you create both then fast.sh will have precedence and fast.php will be ignored.
If you want to add a slow test create the unit test directory as src/test/test-XXX , then add either slow.php or slow.sh . If you create both then slow.sh will have precedence and slow.php will be ignored.
You usually only need to supply a shell script if your unit tests require multiple processes to work. That can happen when you need to test different constant definitions. As you can't redefine constants in PHP you have to restart your process if you want to run with different values.
See existing unit tests for examples of how to use the simple unit test host.
I have heard of and used PHPUnit (although I haven't used it for a long while). I don't use it in this project because I don't feel I need it or that it adds much value. Tests are a shell script, if that's missing they're a PHP script. If I need to make assertions I call assert(). Mudah.
Here are some notes about the various idioms and approaches taken in this library.
In the code you will see things like this:
protected final function is_valid_settings( int $setting_a, string $setting_b ) : bool {
if ( strlen( $setting_b ) > 20 ) { return false; }
return $this->do_is_valid_settings( $setting_a, $setting_b );
}
protected function do_is_valid_settings( $setting_a, $setting_b ) {
if ( $setting_a < 100 ) { return false; }
if ( strlen( $setting_b ) > 10 ) { return false; }
return 1;
}
There are several things to note about this idiom.
In talking about the above code we will call the first function is_valid_settings() the "final wrapper" (or sometimes the "main function') and we call the second function do_is_valid_settings() the "default implementation".
The first thing to note is that the final wrapper is_valid_settings() is declared final and thus cannot be overridden by implementations; and the second thing to note is that the final wrapper declares the data types on its interface.
In contrast the default implementation do_is_valid_settings() is not marked as final, and it does not declare the types on its interface.
This is an example of Postel's Law, which is also known as the Robustness Principle. The final wrapper is liberal in what it accepts, such as with the return value one ( 1 ) from the default implementation; and conservative in what it does, such as always returning a properly typed boolean value and always providing values of the correct type to the default implementation.
Not needing to write out and declare the types on the interface of the default implementation also makes implementation and debugging easier, as there's less code to write. (Also I find the syntax for return types a bit ugly and have a preference for avoiding it when possible, but that's a trivial matter.)
Ordinarily users of this code will only call the main function is_valid_settings() , and anyone implementing new code only needs to override do_is_valid_settings() .
In general you should always wrap any non-final methods (except for private ones) with a final method per this idiom, so that you can have callers override functionality as they may want to do but retain the ability to maintain standards as you may want to do.
If you're refactoring a private method to make it public or protected be sure to introduce the associated final wrapper.
One last thing: if your component has a public function, it should probably be a final wrapper and just defer to a default implementation.
Default implementations should pretty much always be protected, certainly not public, and maybe private if you're not ready to expose the implementation yet.
Having types on the interface of the final method is_valid_settings() confers three main advantages.
The first is that the interface is strongly typed, which means your callers can know what to expect and PHP can take care of fixing up some of the smaller details for us.
The second advantage of this approach is that our final wrapper function is marked as final. This means that the implementer can maintain particular standards within the library and be assured that those standards haven't been elided, accidentally or otherwise.
Having code that you rely on marked as final helps you to reason about the possible states of your component. In the example given above the requirement that $setting_b is less than or equal to 20 bytes in length is a requirement that cannot be changed by implementations; implementations can only make the requirements stronger, such as is done in the default implementation given in the example, where the maximum length is reduced further to 10 bytes.
Another advantage of the typed interface is that it provides extra information which can be automatically added into the documentation. The typed interface communicates intent to the PHP run-time but also to other programmers reading, using, or maintaining the code.
Not having types on the interface of the default implementation do_is_valid_settings() confers four main advantages.
The first is that it's easier to type out and maintain the overriding function as you don't need to worry about writing out the types.
Also, in future, the is_valid_settings() might declare a new interface and change its types. If this happens it can maintain support for both old and new do_is_valid_settings() implementations without implementers necessarily needing to update their code.
The third advantage of an untyped interface for the do_is_valid_settings() function is that it allows for the injection of "impossible" values. These are values which will never be able to make it past the types declared on the main function is_valid_settings() and into the do_is_valid_settings() function, and being able to inject such "impossible" values can make unit testing of particular situations easier, as you can pass in a value that could never possibly occur in production in order to signal something from the test in question.
The fourth and perhaps most important implication of the approach to the default implementation is that it is not marked as final which means that programmers inheriting from your class can provide a new implementation, thereby replacing, or augmenting, the default implementation.
One way a programmer can go wrong is to infinitely recurse. For example like this:
class InfiniteRecursion extends KickassCryptoOpenSslKickassOpenSslRoundTrip {
protected function do_encrypt( $input ) {
return $this->encrypt( $input );
}
}
If the do_encrypt() function calls the encrypt() function, the encrypt() function will call the do_encrypt() function, and then off we go to infinity.
If you do this and you have Xdebug installed and enabled that will limit the call depth to 256 by default. If you don't have Xdebug installed and enabled PHP will just start recurring and will continue to do so until it hits its memory limit or runs out of RAM.
Since there's pretty much nothing this library can do to stop programmers from accidentally writing code like the above what we do is to detect when it's probably happened by tracking how deep our calls are nested using an enter/leave discipline, like this:
try {
$this->enter( __FUNCTION__ );
// 2023-04-07 jj5 - do work...
return $result;
}
catch ( AssertionError $ex ) {
throw $ex;
}
catch ( Throwable $ex ) {
try {
$this->handle( $ex, __FILE__, __LINE__, __FUNCTION__ );
}
catch ( Throwable $ignore ) {
try {
$this->ignore( $ignore, __FILE__, __LINE__, __FUNCTION__ );
}
catch ( Throwable $ignore ) { ; }
}
}
finally {
try { $this->leave( __FUNCTION__ ); } catch ( Throwable $ignore ) { ; }
}
The leave() function has no business throwing an exception, but we wrap it in a try-catch block just in case.
The example code above is shown with typical catch blocks included, but the key point is that the very first thing we do is register the function entry with the call to enter() and then in our finally block we register the function exit with the call to leave() .
If a function enters more than the number of times allowed by KICKASS_CRYPTO_RECURSION_LIMIT without leaving then an exception is thrown in order to break the recursion. At the time of writing KICKASS_CRYPTO_RECURSION_LIMIT is defined as 100, which is less than the Xdebug limit of 256, which means we should always be able to break our own recursive loops.
And for all the trouble we've gone to if the inheritor calls themselves and recurs directly there is nothing to be done:
class EpicFail extends KickassCryptoOpenSslKickassOpenSslRoundTrip {
protected function do_encrypt( $input ) {
return $this->do_encrypt( $input );
}
}
As mentioned above and elaborated on in the following section this library won't usually throw exceptions from the methods on its public interface because we don't want to leak secrets from our call stack if there's a problem.
Instead of throwing exceptions the methods on the classes in this library will usually return false instead, or some other invalid value such as null or [] .
The avoidance of exceptions is only a firm rule for sensitive function calls which handle secret keys, passphrases, unencrypted content, or any other sensitive data. At the time of writing it's possible for the public get_error_list() function to throw an exception if the implementer has returned an invalid value from do_get_error_list() , apart from in that specific and hopefully unlikely situation everything else should be exception safe and use the boolean value false (or another appropriate sentinel value) to communicate errors to the caller.
Sometimes because of the nature of a typed interface it's not possible to return the boolean value false and in some circumstances the empty string ( '' ), an empty array ( [] ), null ( null ), the floating-point value zero ( 0.0 ), or the integer zero ( 0 ) or minus one ( -1 ) may be returned instead; however, returning false is definitely preferred if it's possible.
Aside: in some cases minus one ( -1 ) can be used as the sentinel value to signal an error, such as when you want to indicate an invalid array index or an invalid count, but unlike in some other languages in PHP minus one isn't necessarily an invalid array index, and returning false is still preferred. This library does use minus one in some cases, if there's a problem with managing the telemetry counters.
The fact that an error has occurred can be registered with your component by a call to error() so that if the callers get a false return value they can interrogate your component with a call to get_error() or get_error_list() to get the recent errors (the caller can clear these errors with clear_error() too).
In our library the function for registering that an error has occurred is the error() function defined in the KickassCrypto class.
In some error situations the best and safest thing to do is swallow the error and return a sensible and safe and uncontroversial default value as a fallback.
Here's a quick run-down:
get_error_list() you get an exception with no errorget_error() you get null and an errorclear_error() it's void but with an errorhandle() you get a log entry, no errornotify() it will be handled then ignored, no errorignore() you get a log entry, no errorthrow() it will throw anywayerror() your error may not be properly registered, it always returns falsecount_*() counter you get -1 and no errorincrement_counter() you get -1 and no errorget_const_data_format() you get an empty string and no errorget_const_*() constant accessor you get the value defined by the default constant and no errorget_config_*() config accessor you get the value defined by the default constant (or false if there is no such thing) and no errorget_const() you get the default value and no errorget_passphrase_list() you get an empty array and an errorget_encryption_passphrase() you get null and no erroris_*() method you will get false and no errorget_data_encoding() you will get an empty string and no errorget_data_format() you will get false and no errorconvert_secret_to_passphrase() you will get false and no errorget_padding() you will get false and no errorget_delay() you will get false and no error (an emergency delay will be injected)delay() you will get void and no error (an emergency delay will be injected)log_error() you will get false and no error (but we try to be forgiving)This library is very particular about exception handling and error reporting.
If you have sensitive data on your call stack you must not throw exceptions. Sensitive data includes:
If you encounter a situation from which you cannot continue processing of the typical and expected program logic the way to register this problem is by calling the error() function with a string identifying and describing the problem and then returning false to indicate failure.
As the error() function always returns the boolean value false you can usually register the error and return false on the same like, like this:
return $this->error( __FUNCTION__, 'something bad happened.' );
When I nominate error strings I usually start them with a lowercase letter and end them with a period.
Note that it's okay to intercept and rethrow PHP AssertionError exceptions. These should only ever occur during development and not in production. If you're calling code you don't trust you might not wish to rethrow AssertionError exceptions, but if you're calling code you don't trust you've probably got bigger problems in life.
If you have a strong opinion regarding AssertionError exceptions and think I should not rethrow them I would be happy to hear from you to understand your concern and potentially address the issue.
Following is some example code showing how to handle exceptions and manage errors.
protected final function do_work_with_secret( $secret ) {
try {
$result = str_repeat( $secret, 2 );
$this->call_some_function_you_might_not_control( $result );
return $result;
}
catch ( AssertionError $ex ) {
throw $ex;
}
catch ( Throwable $ex ) {
try {
$this->handle( $ex, __FILE__, __LINE__, __FUNCTION__ );
}
catch ( Throwable $ignore ) {
try {
$this->ignore( $ignore, __FILE__, __LINE__, __FUNCTION__ );
}
catch ( Throwable $ignore ) { ; }
}
}
try {
return $this->error( __FUNCTION__, 'error working with string.' );
}
catch ( Throwable $ignore ) {
try {
$this->ignore( $ignore, __FILE__, __LINE__, __FUNCTION__ );
}
catch ( Throwable $ignore ) { ; }
}
return false;
}
In actual code you would define an error constant for use instead of the string literal 'error working with string.' . In this library the names of error constants begin with "KICKASS_CRYPTO_ERROR_" and they are defined in the src/code/global/constant/framework.php file.
Note that we don't even assume it's safe to call handle() , ignore() , or error() ; we wrap all such calls in try-catch handlers too. There are some edge case situations where even these functions which are supposed to be thread safe can lead to exceptions, such as when there's infinite recursion which gets aborted by the run-time. If you're an expert on such matters the code might do with a review from you.
Now I will agree that the above code is kind of insane, it's just that it seems to me like there's no avoiding it if we want to be safe. We have to explicitly allow the AssertionError exception every single time in every single method just so that assertions remain useful to us as a development tool, and then when we handle other exceptions we want to make some noise about them so we call handle() , but the thing is that handle() will defer to do_handle() which can be overridden by implementers, which means it can throw... so if handle() throws we don't want to just do nothing, we want to give the programmer a last chance to learn of their errant code, so we notify that we're going to ignore the exception with a call to ignore() , but that will defer to do_ignore() , which the programmer could override, and throw from... but if that happens we will just silently ignore such a problem.
And then if we get through all of that and our function hasn't returned then that's an error situation so we want to notify the error, but error() defers to do_error() and that could be overridden and throw, so we wrap in a try-catch block and then do the exception ignore dance again.
I mean it's all over the top and excessive but it should at least be safe and it meets two requirements:
In the usual happy code path none of the exception handling code even runs.
There are a bunch of functions for testing boolean conditions, and they begin with "is_" and return a boolean. These functions should only do the test and return true or false, they should not register errors using the error() function, if that's necessary the caller will do that.
The is_() functions can be implemented using the typed final wrapper idiom documented above.
Following is a good example from the code.
protected final function is_valid_secret( $secret ) : bool {
try {
$is_valid = $this->do_is_valid_secret( $secret );
// ...
assert( is_bool( $is_valid ) );
return $is_valid;
}
catch ( AssertionError $ex ) {
throw $ex;
}
catch ( Throwable $ex ) {
try {
$this->handle( $ex, __FILE__, __LINE__, __FUNCTION__ );
}
catch ( Throwable $ignore ) {
$this->ignore( $ignore, __FILE__, __LINE__, __FUNCTION__ );
}
}
return false;
}
Note that do_is_valid_secret() also has a secret on the call stack, so it should be implemented as exception safe in the same way (in case it is called directly from some other part of the code).
Note too that it's okay to just rethrow assertion violations, these should never happen in production and they make testing the code easier.
The approach to unit-testing taken by this library is simple and powerful. There are three types of test which can be defined for each unit test:
Each script will be either a shell script with the same name, eg fast.sh , or if that's missing a PHP script with the same name, eg fast.php . The test runner just finds these scripts and runs them. This is easy to do and provides all the power we need to run our tests, including support for the various situations where each test instance needs to run in its own process and be isolated from other testing environments.
If you have flakey and unreliable tests you can stick them in as silly tests. The fast and slow tests are the important ones, and you shouldn't put slow tests in the fast test scripts. The fast tests are for day to day programming and testing and the slow scripts are for running prior to a version release.
Here are some notes regarding notable components:
config.php file for the demoSome countries have banned the import or use of strong cryptography, such as 256 bit AES.
Please be advised that this library does not contain cryptographic functions, they are provided by your PHP implementation.
Copyright (c) 2023 John Elliot V.
This code is licensed under the MIT License.
See the contributors file.
I should probably be more disciplined with my commit messages... if this library matures and gets widely used I will try to be more careful with my commits.
The Kickass Crypto ASCII banner is in the Graffiti font courtesy of TAAG.
The string "kickass" appears in the source code 1,506 times (including the ASCII banners).
SLOC and file count reports generated using David A. Wheeler's 'SLOCCount'.
Saya ingin mendengar dari Anda! Hit me up at [email protected]. Put "Kickass Crypto" in the subject line to make it past my mail filters.