2023年頃の現代PHP暗号図書館。
概要:
このライブラリは、PHPナトリウムライブラリとPHP OpenSSLライブラリの周りのラッパーです。
このライブラリのナトリウムコードは、php natium_crypto_secretbox()関数のドキュメントに記載されている例に基づいています。
このライブラリのOpenSSLコードは、PHP opensSl_encrypt()関数のドキュメントに示されている例に基づいています。
このライブラリは、暗号化するデータがそれを暗号化するために使用した秘密のキーと同じくらい安全であることを保証することを目的としています。また、暗号化されたデータが改ざんされていることを保証するために、測定が行われます。
このライブラリは、主要な管理の難しい問題を解決することはできません。

このライブラリは進行中の作業です。
私はこのコードを友人や同僚と共有し、できるだけ多くの批判とフィードバックを求めています。このライブラリができる限り良いと感じたら、このステータスノートを更新します。その間、壊れた変化はほぼ確実であり、暗号化の弱点はかなり可能です。あなたが私が知っておくべきだと思う何かを見つけたら、私に知らせてください!
このライブラリは大きく複雑で、あまり役に立たないことを明確にしたいと思います。まだ発見されていない微妙なバグが積まれていることは間違いありません。このコードベースには、堅実で信頼できるツールに成熟する可能性があると思いますが、プロセスを経る必要があります。
このセクションをお読みください。
暗号コードを間違えることができる方法はたくさんあります。このライブラリは、暗号のフットガンを減らす試みとして書かれています。うまくいけば、それは何も紹介していません!
暗号について最初に知っておくべきことは、あなたのデータがあなたの鍵と同じくらい安全であるということです。私がここであなたに言うことができる以上のことを知っておくべきことがあります(そして、私はとにかく専門家ではありません)が、ここに考えるべきいくつかのことがあります:
注意すべき他のいくつかのこと:
get_error()をかけて、nullを確認することを確認する必要があります)。もう一つのことは、私がそれを学んだときに私を驚かせましたが、あなたが知っていればそれは非常に明白ですが、あなたがそれを暗号化する前にあなたのデータを圧縮してはいけないということです。これは必ずしも問題ではありませんが、特定の状況ではそうなる可能性があるため、おそらくそれをしないことが最善です。
圧縮の問題は、攻撃者が入力データの一部を制御できる場合、特定の値を含めることができ、その後、出力のサイズが減少した場合、他の入力も特定の値に含まれることを知ることができます。痛い。
このコードベースは成熟していないか、よくテストされていません。使用する前に、すべてのコードを読んで、品質基準を満たしていることを確認する必要があります。もしそうなら、私はあなたから聞いて喜んでいます。
誰もが知っておくべき他のことを考えて注意することができるなら、私に知らせてください!
rtfmをしたくない..?そして、私はここにいます、これらすべてのものを書いています...シーシュ。少なくとも上記の警告を読んでください。
#!/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
少し詳しく説明するためには、サンプルコードをチェックしてください。
または、このライブラリがどのように機能するかについての最終結果が必要な場合は、ライブラリフレームワークまたは他のコードのコードを読み取ります。
ねえ、それは十分に始まっただけですが、最終的には複雑になりました。
私は、比較的安全な方法で、比較的安全な並行性コントロールのための比較的敏感なデータ(楽観的な並行性制御のための行バージョン番号)を比較的安全な方法で締めくくりたいと思っていました。
OpenSSLライブラリがPHPで利用できると聞いていたので、それをどのように使用するかに関する情報を検索しました。 openssl_encrypt()関数のPHPドキュメントに例を見つけました。
当初、このコードの使用方法は明確ではありませんでした。特に、認証タグ、初期化ベクトル、および暗号テキストの3つの部分をどうするかを理解することは困難でした。最終的に、私はそれらを連結することができるとわかった。しかし、もし私がそれをするなら、後でそれらを取得できるように、それらの長さと配置を標準化する必要があります...
...そして、私は実際のデータサイズを特定の境界で固定された長さにパディングしてマスクする方が良いと思ったので、私はそれをしました...
...そして、何らかの形のシリアル化を要求する豊富なデータをサポートしたかったのです。最初はPHP Serialize()関数を使用していましたが、後でjson_encode()に変更されました。
サンプルコードは、サポートされている方法でキーを回転させる方法について何も示していませんでした。そこで、往復シナリオとレストアットレストのシナリオのために、主要な管理に対するさまざまなアプローチを備えたこのライブラリでサポートされている2つのユースケースを思いつきました。このライブラリを使用すると、古いキーのサポートを維持しながら、新しいキーで回転できます。
次に、例外の取り扱いとエラーの報告、いくつかのユニットテストと検証、タイミング攻撃緩和、サービスロケーター、使用法のデモ、データサイズ制限、パスフレーズの初期化、キー生成スクリプト、テレメトリなどに慎重にアプローチしました。
基本的に、このライブラリ全体は、ビルトインPHP OpenSSLライブラリの実装を実際に使用できるように、私がしなければならないと感じたすべてのものでした。
そして...人々はナトリウムライブラリについて私に話し始め、私が代わりにそれを使用することを提案し始めました。私はすでに重要な管理と入力のシリアル化とメッセージのフォーマットとエンコードのためにたくさんの仕事をしているので、私はそのすべてを再利用して、ナトリウムの周りのラッパーを提供できると考えました。それが私がしたことです。
このライブラリを使用する場合、ナトリウムの実装を使用するか、OpenSSL実装を使用するかを決定できます。 2つの実装は喜んで共存できるため、必要に応じてコードを作成するためにコードを作成することもできます。実装は、キー構成やデータ形式を共有することはなく、完全に個別です。 (そうは言っても、暗号化アルゴリズムを切り替えることは正確には些細なことではなく、おそらくすべてのデータを移行するためにオフラインにする必要があります。それができない場合は、悪い時間を過ごすことができるので、アルゴリズムを切り替えることを計画しないでください。
私はこのライブラリが自分の暗号を転がしているとは考えていません。むしろ、実際にナトリウムとopenSSLを使用する方法を考えていると思います。明らかなことやその他の間違いを犯したなら、私はそれについて聞いて本当に感謝しています。
時々更新することを覚えていると仮定すると、ここにデモシステムがあります。
デモ機能は、HTMLとHTTPを使用して、クライアントとサーバーの間で暗号化されたデータを往復する方法を示しています。
デモコードは、自分でホストしたい場合は、このライブラリでSRC/ DEMO/ディレクトリで入手できます。
私がそれらを時々更新することを覚えていると仮定すると、PHPドキュメントはここにあります:
上記のように、このようなコマンドでGitのコードを確認できます。
git clone https://github.com/jj5/kickass-crypto.git
このコードは未発表で、安定したバージョンはありません。
アプリケーションで使用するためにクライアントライブラリを含めたい場合は、INC/NATIUM.PHPまたはINC/OpenSSL.PHPファイルが含まれています。このようなものを使用してください:
require_once __DIR__ . '/lib/kickass-crypto/inc/sodium.php';
このライブラリを読み込んだ後、通常、 kickass_round_trip()またはkickass_at_rest()サービスロケーターを介してアクセスします。
$ciphertext = kickass_round_trip()->encrypt( 'secret text' );
$plaintext = kickass_round_trip()->decrypt( $ciphertext );
echo "the secret data is: $plaintext.n";
物事をそんなに単純にするのに多くの作業が必要でした!
デモコードをホストする場合は、SRC/ DEMO/でファイルをホストし、Project Base Directory(このreadme.mdファイルを含むディレクトリです)に有効なconfig.phpファイルを含める必要があります。デモンストレーションのために有効なconfig.phpファイルは、 CONFIG_SODIUM_SECRET_CURRの定数文字列を定義する必要がありますが、長いランダムな文字列である必要があります。
php bin/gen-key.php
または、以下を備えたデモconfig.phpファイル全体を生成することができます。
php bin/gen-demo-config.php > config.php
ソフトウェアファイルとコード行に関するメモを次に示します。
Total Number of Files = 128
Total Number of Source Code Files = 128
| ディレクトリ | ファイル | 言語によって |
|---|---|---|
| テスト | 63 | php = 59、sh = 4 |
| コード | 35 | php = 35 |
| ビン | 22 | php = 13、sh = 9 |
| Inc | 7 | php = 7 |
| デモ | 1 | php = 1 |
| 言語 | ファイル | パーセンテージ |
|---|---|---|
| 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).
| ディレクトリ | sloc | 言語によって |
|---|---|---|
| コード | 5,136 | php = 5136 |
| テスト | 3,363 | php = 3193、sh = 170 |
| ビン | 603 | php = 423、sh = 180 |
| デモ | 71 | php = 71 |
| Inc | 37 | php = 37 |
| 言語 | sloc | パーセンテージ |
|---|---|---|
| Php | 8,860 | (96.20%) |
| sh | 350 | (3.80%) |
このコードは、PHP 7.4以上で動作するはずです。このコードをPHPの古いバージョンで実行しようとすると、エラーメッセージを記録してプロセスを終了しようとします。
このコードは、64ビットプラットフォームで実行されていることを確認するためにチェックします。そうでない場合は、文句を言って終了します。
ナトリウムモジュールをロードすると、ライブラリはナトリウムライブラリが実際に利用可能であることを確認します。そうでない場合、プロセスは文句を言い、終了します。
OpenSSLモジュールをロードすると、ライブラリはOpenSSLライブラリが実際に利用可能であることを確認します。そうでない場合、プロセスは文句を言い、終了します。
このコードはあらゆるオペレーティングシステムで実行されるべきだと思いますが、Linuxでのみテストしました。あなたがMacosまたはWindowsで成功したなら、私はそれについて喜んで聞いています。
シェルスクリプトはBash用に書かれています。バッシュがない場合は、ポートする必要がある場合があります。
このコードは、2つの特定のユースケースをサポートしています。
キーは、ユースケースごとに別々に異なる方法で管理されます。
各ユースケースのサポート方法の詳細を以下に文書化します。
このライブラリをレストアット暗号化に使用することは、一般に、往復暗号化に単純に使用するよりも大きなリスクであり、より大きなコミットメントです。ラウンドトリップの暗号化キーを紛失した場合、または緊急に回転させることを余儀なくされている場合、それはおそらくあなたのレストキーで同様のことが起こった場合よりも問題になるでしょう。
このライブラリが開発された主なユースケースは、楽観的な並行性コントロールのためのミッションクリティカルな行バージョン番号を含むが、ミッションクリティカルな行バージョン番号を含む数キロバイトのデータをラウンドトリップすることをサポートすることでした。このライブラリの使用は、代替案(楽観的な並行性制御データを暗号化または改ざんしない)と比較して改善されています。他のアプリケーションで本当に適しているかどうかは未解決の質問であるかどうかはわかりません。確かに、このライブラリが必要なレベルのセキュリティを提供しない場合は、使用すべきではありません。
構成ファイルでシークレットを指名する優先およびサポートされた方法は、PHP define()関数を使用した定数としてです。クラス/インスタンスのフィールドまたはグローバル変数を使用することの問題は、値がデバッグとロギングのコードにかなり簡単に漏れることができることです。これは、定数の可能性が低い(まだ可能ですが)。同様に、インスタンスフィールド、クラスフィールド、またはグローバルを使用して、可能であれば、可能であれば、機能内のローカル静的変数を使用する最良の方法は、グローバル/静的データ(構成ファイルからの読み取りなど)をキャッシュする必要がある場合、より簡単に秘密の漏れにつながる可能性があります。
例を挙げると、次のようなdouble-define.phpというテストファイルを作成しましょう。
<?php
define( 'TEST', 123 );
define( 'TEST', 456 );
次に、コードを実行すると、このようなことが起こります。
$ 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
その一定の値にあなたの秘密の鍵が含まれているなら、あなたはちょうど非常に悪い日を過ごしました。
PHPで定数を定義する最も安全な方法は、すでに定義されている定数を定義しようとすると、エラーが発生するため、最初に定義されていないことを確認することです。既に定義されている定数が見つかった場合、エラーメッセージで中止することができます(パブリックWebが表示される可能性があるため、あまり詳細を提供しない場合)か、既存の値を維持し、それを再定義しようとしないでください。 bin/gen-demo-config.php configファイルジェネレーターは、最初のアプローチを取り、複製が検出された場合にphp die()関数を呼び出します。生成されたconfig.phpファイルを2回含めることで何が起こるかを見ることができます。
require __DIR__ . '/config.php';
require __DIR__ . '/config.php';
config-die.phpにconfig.php 2倍に含めると何が起こるかの例を見つけることができます。
その結果、ほとんどのPHPソースファイルと同様に、 config.phpファイルを含めるときにrequire_once使用することをお勧めします。
require_once __DIR__ . '/config.php';
秘密のものに名前を付けたとき、名前に文字列「パス」(「パスワード」、「passWD」、「passphrase」、または「パスポート」または「秘密」のように)が含まれていることを確認します。私の汎用ロギング施設(このライブラリには含まれていません)では、診断データを記録する前に(ケース非感受性)という名前であらゆるものをスクラブして編集します。このプラクティスを採用することをお勧めします。
このライブラリでは、変数または定数に機密データが含まれている場合は、「パス」または「秘密」のいずれかが名前のサブストリングとして命名されます。
機密データをログに記述しないでください。
機密変数、フィールド、または定数の名前に「パス」または「秘密」のいずれかを置きます。
ここでは、このライブラリのコンテキストでこれらの同様のサウンド用語が実際に意味することを説明します。
デフォルトモジュールを使用する場合、データ形式はOpenSSLモジュールの「KA0」またはナトリウムモジュールの「KAS0」のいずれかです。
ベースフレームワークを継承して独自の暗号モジュールを定義する場合、デフォルトのデータ形式は、OpenSSL実装に基づくモジュールのデフォルトデータ形式またはdo_get_const_data_format()実装に基づくモジュールの「XKA0」です。実装。
暗号文を正常に復号化するには、データ形式に適切なモジュールを使用する必要があります。
データエンコーディングは、JSON、PHPシリアル化、またはテキストのいずれかです。データ形式(上記)に適切なモジュールがあると仮定し、以下で説明する警告を使用して、使用したデータをエンコードするデータに関係なく、何でも復号化できます。暗号化は、構成されたデータエンコーディングを使用して行われます。config_encryption_data_encodingを参照してください。
config_encryption_phps_enableも定義しない限り、phpsエンコーディングを使用できないことに注意してください。これは、phpの敏aserializationが安全ではないため、デフォルトで無効になっているためです。正直なところ、これは少し手を振っています。 PHP unserialize()コードインジェクションにつながる可能性があるという噂を聞いたばかりですが、それが本当なのか、それが正確に何を意味するのかはわかりません。私はPHPのシリアル化と脱介入を実装し、少しテストしましたが、それが本当に不安であるかどうかはわかりません。 JSONとテキストデータエンコードは安全であると確信しています。
KickassCryptoから継承し、特定の機能をオーバーライドすることに加えて、構成定数を介して多くの構成を利用できます。 CONFIG_SODIUMを検索して、ナトリウムとCONFIG_OPENSSL利用できるものを見つけて、OpenSSLに利用できるものを見つけます。
現時点では、このコードはconfig.phpファイルで直接構成されていることをアドバイスしてください。
将来、 config.phpファイルには、個別に管理されたconfigファイルが含まれます。
これらのファイルでキーを自動的に回転およびプロビジョニングするための管理スクリプトがあります。
経験豊富なLinuxユーザーは、 /etc/sudoersを直接編集していないことを知っています。visudoでvisudoして、誤って構文エラーを導入していないことを確認してシステムをホースできます。
config.phpおよびその他のconfigファイルを編集および管理するための同様のスクリプトを提供するつもりです。これらの更新のために待機してください。その間...非常に注意してください。
あなたが非常に注意する必要があることの1つは、「.php」ファイル拡張機能を備えたPHPファイル以外のキーを管理することです。キーを「.ini」ファイルなどに配置すると、 Webサーバーによってプレーンテキストとして提供される可能性があります。だからそれをしないでください。また、潜在的なエラーメッセージとともに詳細が漏れている可能性があるため、構成ファイルまたは生産で実行されている他のソースファイルに構文エラーを導入しないように注意してください。
前のセクションで述べたように、名前付き構成定数のサポートによってかなりの量の構成可能性が提供されます。
構成定数に加えて、 KickassCryptoベースクラスから継承し、その方法をオーバーライドする場合、できることがたくさんあります。
構成定数の代替として(プロセスごとに1回しか定義できず、その後変更できません)、構成オプションのget_config_...()およびget_const_...()のようにインスタンスメソッドがあります。最も重要な定数と構成オプションは、これらのアクセサーを介して間接的に読み取られるため、確実にオーバーライドできるはずです。
PHPビルトイン関数へのほとんどの呼び出しは、 KickassCryptoの保護された関数を介して薄いラッパーによって行われます。これらは、 KICKASS_WRAPPER_PHP特性で定義されています。この間接により、特定のPHP関数の呼び出しを傍受し、潜在的に変更することができます。これは主に単体試験中の障害注入をサポートするために行われていますが、他の目的に使用して実装の詳細を変更できます。
KickassCryptoで敏感であると見なされるものは、プライベートまたは最終として定義されます。プライベートではなく、最終的なものではない場合は、オーバーライドするための公正なゲームです(間違いを犯した場合を除きます)。特に、 do_から始まるインスタンスメソッドは、実装者によって交換または傍受されるように特別に作成されました。
このライブラリは、暗号ライブラリのインスタンスをそれぞれ管理する2つのサービスロケーター機能を提供します。
kickass_round_trip()kickass_at_rest()関数を呼び出し、新しいインスタンスを唯一のパラメーターとして渡すことにより、サービスロケーター関数が提供するサービスのインスタンスを置き換えることができます。
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 );
理想的には、このライブラリは、要件を箱から出して(または特定の構成を使用して)満たし、デフォルトでサービスロケーターが提供するインスタンスを交換する必要はありません。
サービスロケーターは、インスタンスがまだない場合、サービスロケーターへの最初の呼び出しで新しいデフォルトインスタンスを作成します。デフォルトの実装がナトリウムモジュールであるかOpenSSLモジュールであるかはinc/sodium.phpおよびinc/openssl.phpファイルを含めた順序によって異なります。 inc/library.phpにライブラリ全体を含めた場合、ナトリウムモジュールに優先されます。
ナトリウムモジュールまたはOpenSSLモジュールのサービスロケーターをロードしたかどうかに関係なく、引数として新しいインスタンスを使用してサービスロケーターを呼び出すことにより、デフォルトのインスタンスをオーバーライドできます。
暗号化プロセスは大まかです。
ナトリウムライブラリは、初期化ベクトル(同様の効果)の代わりに非CEを使用し、ナトリウムは独自の認証タグを処理することに注意してください。
このライブラリがその暗号文をエンコードすると、ナトリウム実装用の「KAS0/」のデータフォーマットプレフィックスとOpenSSL実装には「KA0/」が含まれます。
データフォーマットプレフィックスのゼロ( "0")はバージョンゼロ用です。これは、インターフェイスが不安定であり、変更される可能性があることを暗示することを目的としています。
このライブラリの将来のバージョンは、安定したデータ形式の新しいデータフォーマットプレフィックスを実装する場合があります。
このライブラリが暗号文を解読すると、データフォーマットのプレフィックスを検証します。現在、「kas0/」または「ka0/」のみがサポートされています。
上記のバージョンゼロデータ形式は、現在以下を暗示しています。
データをエンコードした後(デフォルトでは、次のセクションで説明します)、パディングが完了し、データの長さが前に付けられます。暗号化の前に、このようにメッセージがフォーマットされます。
$message = $encoded_data_length . '|json|' . $encoded_data . $this->get_padding( $pad_length );
JSONデータの長さは、8文字の16進数としてフォーマットされています。 8文字のサイズは一定であり、JSONデータの長さの大きさによって異なりません。
パディングの理由は、実際のデータサイズを不明瞭にするためです。パディングは最大4つのKIB境界(2 12バイト)で行われ、チャンクと呼ばれます。チャンクサイズは構成可能であり、デフォルトは将来変更される可能性があります。
次に、ナトリウムで暗号化する場合、メッセージはsodium_crypto_secretbox()で暗号化され、次にciphertextが一緒に連結されます。
$nonce . $ciphertext
それ以外の場合は、OpenSSLで暗号化する場合、メッセージはAES-256-GCMで暗号化され、初期化ベクトル、暗号文、および認証タグが一緒に連結されます。
$iv . $ciphertext . $tag
その後、すべてがphp base64_encode()関数でエンコードされ、データフォーマットプレフィックスが追加されます。
このように行われるナトリウムの場合:
"KAS0/" . base64_encode( $nonce . $ciphertext )
そして、このように行われているopensslのために:
"KA0/" . base64_encode( $iv . $ciphertext . $tag )
復号化プロセスでは、「KAS0」データ形式の24バイトのノンセとciphertextを見つけることが予想されます。
暗号文を復号化した後、ライブラリはJSONデータのサイズを8文字のヘックスエンコード値を表すASCII文字列として見つけると予想しています。その後、単一のパイプ文字が続き、その後4文字データエンコードインジケーター(「JSON」または「PHP」)が続き、その後にJSON(またはPHP Serialized Data)が続きます。ライブラリは、パディングからJSON/シリアル化されたデータを抽出し、残りのデコードを処理できます。
暗号化の前に、入力データはPHP json_encode()関数を使用してJSONとしてエンコードされます。当初、このライブラリはPHP serialize()関数を使用していましたが、明らかにコード解釈シナリオにつながる可能性があるようです(詳細についてはわかりません)。したがって、代わりにJSONエンコードを使用します。
データをエンコード形式として使用することは、サポートできる値に関していくつかの小さな意味を持ちます。特に、オブジェクトインスタンスをオブジェクトインスタンスに戻すことができるオブジェクトインスタンスをエンコードすることはできません(オブジェクトがjsonserializableインターフェイスを実装した場合、データとしてシリアル化できますが、それらはPHPアレイにのみデコードされ、PHPオブジェクトが来たPHPオブジェクトではありません)。いくつかの奇妙な浮動小数点値を表すことはできません(すなわち、nan、pos inf、neg info、and neg zero)。バイナリ文字列はJSONでは表現できません。
デフォルトでは、これらのオプションはJSONエンコードに使用されます。
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
しかし、これらのオプションは、JSONをデコードする実装の能力に影響しません。実装は、data_encode()およびdata_decode()メソッドをオーバーライドすることにより、必要に応じてJSONエンコードとデコードを微調整できます。または、 CONFIG_ENCRYPTION_JSON_ENCODE_OPTIONSおよびCONFIG_ENCRYPTION_JSON_DECODE_OPTIONS定数を使用して、 config.phpファイルのjsonエンコードとデコードオプションをconfig.phpファイルを指名することができます。
define( 'CONFIG_ENCRYPTION_JSON_ENCODE_OPTIONS', JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
define( 'CONFIG_ENCRYPTION_JSON_ENCODE_OPTIONS', JSON_THROW_ON_ERROR );
このライブラリは、 JSON_THROW_ON_ERRORが指定されているかどうかに関係なく機能する必要があります。
JSONエンコードオプションでJSON_PARTIAL_OUTPUT_ON_ERRORを指定した場合、データは静かに無効になる可能性があります。おそらく直感的には、 JSON_PARTIAL_OUTPUT_ON_ERROR有効にすることは、少なくともその場合は何かを得るため、最も最悪の戦略であることがわかりました。 JSON_PARTIAL_OUTPUT_ON_ERRORを有効にしない場合、入力の一部をエンコードできない場合(UTF-8などの有効なエンコードにないバイナリ文字列がある場合など)、データ全体が削除されます。 JSON_PARTIAL_OUTPUT_ON_ERRORでは、表現できない部分のみが省略されます。現時点では、 JSON_PARTIAL_OUTPUT_ON_ERROR自動的に指定されていませんが、これは将来再訪するかもしれないものです。
これらのJSONエンコード/デコードオプションのいずれかを使用すると、最終的には悪い時間があるかもしれません。
JSON_NUMERIC_CHECKJSON_INVALID_UTF8_IGNOREJSON_INVALID_UTF8_SUBSTITUTE このライブラリがデータを暗号化すると、出力を構成可能なチャンクサイズにパッドにパッドします。
チャンクサイズの構成定数はCONFIG_ENCRYPTION_CHUNK_SIZEです。
デフォルトのチャンクサイズは4,096(2 12 )です。
チャンクサイズを8,192に増やしたい場合は、このようなconfig.phpファイルでそれを行うことができます。
define( 'CONFIG_ENCRYPTION_CHUNK_SIZE', 8912 );
定義されたチャンクサイズを変更すると、新しいデータに適用され始め、異なるチャンクサイズで暗号化された古いデータは引き続き復号化できます。
データサイズの制限が観察される限り(これらについては次に説明します)、このライブラリはPHPによってJSONとしてエンコードできるものを暗号化できます。
これには、次のようなさまざまなものが含まれます。
JSONでサポートできないもの:
ブール値Falseを暗号化できないことに注意してください。暗号化できなかったからではなく、復号化が失敗したときに返品するからです。したがって、falseを暗号化することを拒否して、復号化時にエラーと混同できないようにします。
ブール値を暗号化する必要がある場合は、次のような配列に置くことを検討してください。
$input = [ 'value' => false ];
またはJSONとしてエンコード、次のように:
$input = json_encode( false );
これらのどちらかを行うと、価値を暗号化できます。
PHPでは、「文字列」は本質的にバイト配列であることを指摘する価値があります。つまり、本質的に「バイナリ」データを含めることができます。ただし、このようなバイナリデータはJSONとして表すことはできません。バイナリデータを処理する必要がある場合、最良の方法は、おそらくBase64_Encode()を使用してbase64またはbin2hex()を使用してhexadecimalとしてエンコードしてから、それを暗号化することです。
将来的には、必ずしもJSONエンコードされていないデータを操作する機能がこのライブラリに追加される可能性があります。それがあなたが持っている機能であるかどうか教えてください。
注:JSONエンコードの代わりにPHPシリアル化を使用することがオプションになりました。このドキュメントは、それがどのように機能し、どのように使用するかを説明するために更新する必要があります。 PHPシリアル化の利点は、JSONができるよりも多くのデータ型と形式をサポートすることです。
データがJSONとしてエンコードされた後、構成可能な最大長に制限されます。
最大JSONエンコーディング長の構成定数はCONFIG_ENCRYPTION_DATA_LENGTH_MAXです。
デフォルトのデータエンコード制限は67,108,864(2^ 26 )バイトで、約67 MBまたは64 MIBです。
より大きくする必要がある場合は、このデータエンコード制限を構成することができます。制限が大きすぎると、メモリの問題が発生し、プロセスが終了する可能性があることに注意してください。
データエンコード制限を減らしたい場合は、このようなconfig.phpファイルでそれを行うことができます。
define( 'CONFIG_ENCRYPTION_DATA_LENGTH_MAX', pow( 2, 25 ) );
このライブラリは、圧縮が犯罪SSL/TLS攻撃などの暗号化の弱点を導入できるため、入力データを圧縮しません。
問題は、攻撃者がプレーンテキストの一部を変更できる場合、入力データがプレーンテキストの他の部分に存在するかどうかを確認できることです。
攻撃者が秘密の他のデータに供給できるデータを圧縮しないことが非常に重要です。まったく圧縮しないのが最善です。
暗号化または復号化中にエラーが発生した場合、1ミリ秒(1 ms)から10秒(10秒)の遅延が導入されます。これは、潜在的なタイミング攻撃に対する緩和です。ディスカッションについては、S2Nとラッキー13を参照してください。
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().簡単。
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'.
I'd love to hear from you! Hit me up at [email protected]. Put "Kickass Crypto" in the subject line to make it past my mail filters.