大约2023年的当代PHP加密图书馆。
概要:
该库是PHP钠库和PHP OpenSSL库围绕的包装器。
该库中的钠代码基于PHP Sodium_crypto_secretbox()函数的文档中给出的示例。
此库中的OPENSL代码基于PHP OPENSSL_ENCRYPT()函数的文档中给出的示例。
该库旨在确保其加密数据与用于加密的秘密密钥一样安全。还采取了措施,以确保加密数据具有防篡改。
该库无法解决钥匙管理的严重问题。

这个库是一项正在进行的工作。
我正在与朋友和同事分享此代码,并尽可能多地批评和反馈。当我觉得这个库尽我所能做到的好时,我将更新此状态注释。同时,打破变化几乎是确定的,并且加密弱点是非常有可能的。如果您发现我认为我应该知道的东西,请告诉我!
我想和您清楚地说,这个库很复杂,而且没有太多用处。毫无疑问,尚未发现的微妙虫子。我认为,此代码基础有可能成熟成一个可靠的可靠工具,但是我们需要进行整个过程。
请阅读本节。
您的加密代码有很多方法可以出错。该图书馆是为了减少加密富枪的尝试。希望它没有介绍!
关于加密货币的第一件事是,您的数据仅与密钥一样安全。关于关键管理的知识比我在这里告诉您的要多得多(无论如何我都不是专家),但是这里有几件事要考虑:
其他一些要注意的事情:
get_error()以确保其无错误表示错误)。当我学到的时候,这让我感到惊讶的另一件事,尽管一旦知道,这很明显,但它是在加密数据之前不应压缩数据的。这并不总是一个问题,但是在某些情况下可能是,所以最好不要这样做。
压缩的问题是,如果攻击者可以控制某些输入数据,则可以包括特定值,然后如果输出尺寸降低,则可以知道特定值中的其他输入也包括。哎哟。
此代码库不是成熟或测试良好的,在使用它之前,您应该阅读所有代码,以确保它符合您的质量标准。如果您这样做,我很高兴收到您的来信。
如果您能想到每个人都应该知道的其他任何东西,请小心,请告诉我!
不想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文档中找到了示例代码。
最初,我不清楚如何使用此代码。特别是很难弄清楚如何处理这三个部分:身份验证标签,初始化向量和密码文本。最终,我发现我可以加入它们。但是,如果我要这样做,我需要对它们的长度和放置进行标准化,以便以后可以找回它们...
...然后我认为最好通过将其填充到某些边界的固定长度来掩盖我的实际数据大小,所以我做到了...
...然后我想支持需要某种序列化的丰富数据。最初,我使用的是php serialize()函数,但后来已更改为json_encode()。
示例代码没有任何关于如何以支持方式旋转键的任何信息。因此,我想出了该库支持的两个用例,采用不同的方法来管理往返和精加工方案的钥匙管理方法。该库使您可以在新的钥匙中旋转,同时保持对旧密钥的支持,因为您可能不会做。
然后,我以仔细的方法进行了层次,以进行异常处理和错误报告,某些单元测试和验证,时机攻击缓解,服务定位器,使用情况演示,数据大小限制,密码初始化,密钥生成脚本,遥测等。
基本上,整个库只是我必须做的一切,以便我实际上可以使用内置的PHP OpenSSL库实现。
然后...人们开始告诉我有关钠库的信息,并建议我使用它。由于我已经为密钥管理和输入序列化以及消息格式化和编码做了很多工作,因此,我认为我可以重复使用所有这些,并在钠周围提供包装。这就是我所做的。
现在,如果您使用此库,则可以决定是要使用钠实现还是OPENSL实现。因为这两个实现可以愉快地共存,因此,如果您需要的话,也可以编写代码从一个移动到另一个。实现从未共享关键配置或数据格式,它们是完全分开的。 (也就是说,切换加密算法并不是完全微不足道的,您可能必须离线迁移所有数据,如果您不能这样做,那么您将有一个糟糕的时间,因此如果不确定您不确定要开始使用钠并坚持下去,请不要打算切换算法。
我认为这个库不滚动我自己的加密货币,而是我认为这是弄清楚如何实际使用钠和openssl的。如果我犯了任何明显的错误,我真的很高兴听到有关它的消息。
假设我记得不时更新它,这里有一个演示系统:
演示设施仅显示了如何使用HTML和HTTP在客户端和服务器之间进行加密数据。
如果您想自己托管,则该库中的演示代码可在此库中可用。
假设我记得不时更新它们,则PHP文档在这里:
如上所述,您可以使用这样的命令查看Git中的代码:
git clone https://github.com/jj5/kickass-crypto.git
此代码未发行,没有稳定的版本。
如果要将客户库包括在应用程序中使用的客户库,包括Inc/Sodium.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/ emo/中托管文件,并在项目基础目录中包含有效的config.php文件(这是包括此readme.md文件的目录)。 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位平台上运行。如果不是,它将抱怨和退出。
如果加载钠模块,库将确保钠库实际上可用。如果不是这样,该过程将抱怨和退出。
如果加载OPENSL模块,库将确保实际上可用的OpenSSL库。如果不是这样,该过程将抱怨和退出。
我相信该代码应该在任何操作系统上运行,但是我只在Linux上对其进行了测试。如果您在MacOS或Windows上取得了成功,我很高兴听到它。
外壳脚本是为bash编写的。如果您没有bash,则可能需要端口。
该代码支持两个特定用例:
对于每个用例,键分别管理和不同。
下面记录了如何支持每个用例的详细信息。
将此库进行精加工加密通常是更大的风险和更大的承诺,而不是仅仅用于往返加密。如果您丢失了往返加密密钥或被迫紧急旋转它们,那可能会比您的当下键发生类似的事情要小。
开发该库的主要用例是支持几千字节的数据,其中包含轻度敏感但没有任务的关键行版本编号,以进行乐观并发控制。与替代方案(不加密或篡改乐观的并发控制数据)相比,该库的使用是一种改进。我不确定,它是否真的适合其他应用程序是一个悬而未决的问题。当然,如果该库不提供所需的安全级别,则不应使用该库。
在配置文件中提名秘密的首选和支持的方法是使用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中常数的最安全方法是检查它尚未首先定义,因为尝试定义已经定义的常数将导致错误。如果您找到已经定义的常数,则可以使用错误消息中止(如果您没有提供太多的细节,因为公共网络可能会看到它),或者只是保留现有的值,并且不要尝试重新定义它。 bin/gen-demo-config.php配置文件生成器采用第一种方法,并在检测到重复时调用php die()函数。您可以通过两次包括生成的config.php文件来查看会发生什么,as:
require __DIR__ . '/config.php';
require __DIR__ . '/config.php';
您可以找到一个示例,说明如果将config.php包含在config-die.php中。
因此,与大多数PHP源文件一样,在包含config.php文件时最好使用require_once :
require_once __DIR__ . '/config.php';
当我命名秘密的东西时,我确保名称包含字符串“通行证”(如“密码”,“ passwd”和“ passphrase”,甚至在某种程度上,“ passport”,“ passport”)或“秘密”。在我的通用日志记录设施中(本库中不包含),我在记录诊断数据之前擦洗并用匹配的名称(不敏感)的名称。我鼓励您采用这种做法。
在此库中,如果变量或常数可能包含敏感数据,它将以“通行证”或“秘密”为名命名。
不要将敏感数据写入日志中。
请以敏感变量,字段或常数的名义将“通过”或“秘密”放置。
在这里,我解释了这些类似的声音术语在此库的背景下实际上是什么意思。
如果使用默认模块,则数据格式为OPENSL模块的“ KA0”,或者是钠模块的“ KAS0”。
如果您继承了基本框架并定义了自己的加密模块,则基于OpenSL实现的模块的默认数据格式为“ XKA0”,或基于sodium实现的模块的模块或“ XKAS0”,否则您的实现了您的do_get_const_data_format() the do not do do do not do note note n.官方实施。
您需要将正确的模块用于数据格式,以便成功解密密文。
数据编码是JSON,PHP序列化或文本。假设您具有适合数据格式的正确模块(上图),并且在下面讨论的警告中,无论使用该数据编码如何,您都可以解密任何内容。将使用配置的数据编码进行加密,请参见Config_encryption_data_encoding,它可以是:
请注意,除非您还定义了config_encryption_phps_enable,否则您将无法使用PHP编码,这是因为PHP避难所可能是不安全的,因此默认情况下它被禁用。老实说,这有点手浪。我刚刚听到有传言称PHP unserialize()可能会导致代码注入,但我不确定这是真的还是它的含义。我实施了PHP序列化和避免序列化,并进行了一些测试,但我不知道它是否真的没有安全感。我很确定JSON和文本数据编码应该是安全的。
除了从KickassCrypto继承和覆盖特定功能外,还可以通过配置常数获得很多配置。搜索CONFIG_SODIUM以查找可用于钠和CONFIG_OPENSSL的可用内容,以找到可用于OpenSSL的东西。
请注意,目前此代码直接在config.php文件中配置。
将来, config.php文件将包含单独管理的配置文件,为:
这些文件将有用于自动旋转和配置密钥的管理脚本。
经验丰富的Linux用户知道您不直接编辑/etc/sudoers ,而是使用visudo进行编辑,以便您可以验证您没有意外地引入语法错误并使用系统。
我打算提供类似的脚本来编辑和管理config.php和其他配置文件。因此,这些更新。同时...请非常小心。
您应该非常小心的一件事是不要在使用“ .php”文件扩展名的PHP文件中管理密钥。如果您将键放入“ .ini”文件或类似文件中,则它们很可能会由Web服务器用作纯文本。所以不要那样做。另外,请注意不要将语法错误引入您的配置文件或生产中运行的其他源文件,因为详细信息可能会随着潜在的错误消息而泄漏。
如前一节所述,支持命名配置常数提供了相当多的可配置性。
除了配置常数外,如果您从KickassCrypto基类继承并覆盖其方法,您还可以做很多事情。
作为配置常数的替代方案(只能每个过程只能定义一次,然后无法更改),对于配置选项,有实例方法为get_config_...() get_const_...()用于常量评估。最重要的常数和配置选项是通过这些登录器间接读取的,因此您应该能够可靠地覆盖它们。
大多数对PHP内置功能的调用都是由薄包装器通过KickassCrypto上的受保护功能完成的。这些定义在KICKASS_WRAPPER_PHP特征中。这种间接允许某些PHP函数调用被拦截并可能修改。这样做的主要目的是支持单位测试期间的故障注入,但是您可以用于其他目的来更改实施细节。
在KickassCrypto中被认为敏感的事物被定义为私人或最终。如果它不是私人的,并且不是最终的,那是公平的比赛(除非我犯了一个错误)。特别是以do_开头的实例方法是专门为实施者替换或拦截的。
该库提供了两个服务定位器函数,该功能管理每个加密库库的实例,它们是:
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 );
理想情况下,此库将满足您的要求(或具有某些配置),并且默认情况下您无需替换服务定位者提供的实例。
如果服务定位器尚未有实例,则在第一个呼叫服务定位器的第一个调用中,将为您创建一个新的默认实例。默认实现是钠模块还是OPENSL模块取决于您包括inc/sodium.php和inc/openssl.php文件的顺序;如果您将整个库与inc/library.php一起包含,则钠模块将具有优先级。
无论您是为钠模块加载服务定位器还是OPENSSL模块,您都可以通过以新实例为参数来调用服务定位器来覆盖默认实例。
加密过程大致:
请注意,钠库使用非CE代替初始化向量(相似的效果),钠可以处理其自己的身份验证标签。
当此库编码其密文时,它包含用于钠实现的“ Kas0/”的数据形式前缀,以及用于OpenSSL实现的“ KA0/”。
Data-Format前缀中的零(“ 0”)用于零版本,这旨在暗示接口不稳定并且可能会更改。
该库的未来版本可能会为稳定的数据格式实现新的数据形式前缀。
当此库解码其密文时,它会验证数据格式前缀。目前仅支持“ KAS0/”或“ KA0/”。
上述版本零数据格式目前表示以下内容:
在数据编码(默认情况下,在下一节中讨论)进行填充后,将完成数据长度。在加密之前,邮件的格式是这样:
$message = $encoded_data_length . '|json|' . $encoded_data . $this->get_padding( $pad_length );
JSON数据长度格式为8个字符十六进制值。 8个字符的大小是恒定的,并且根据JSON数据长度的大小而变化。
填充的原因是要掩盖实际的数据大小。填充最多可在4 KIB边界(2 12个字节)中完成,我们称之为块。块大小是可配置的,默认值可能会在将来发生变化。
然后,如果我们要使用钠加密,则消息将使用sodium_crypto_secretbox()加密,然后将nonce和Ciphertext加入在一起,如下:
$nonce . $ciphertext
否则,如果我们使用openssl加密,则消息将使用AES-256-GCM进行加密,并且初始化向量,密文和身份验证标签被串联在一起,如下所示:
$iv . $ciphertext . $tag
然后,所有内容都是用php base64_encode()函数编码的base64,并添加了data-Format前缀。
对于这样做的钠:
"KAS0/" . base64_encode( $nonce . $ciphertext )
对于这样做的openssl:
"KA0/" . base64_encode( $iv . $ciphertext . $tag )
解密过程希望找到“ KAS0”数据格式的24个字节NONCE和CIPHERTEXT,以及12个字节初始化向量,Ciphertext和KA0数据格式的16个字节身份验证标签。
在解密密码后,图书馆希望找到JSON数据的大小为ASCII字符串,代表8个字符的六角形编码值,其次是单个管道字符,然后是一个四个字符数据编码指示灯('json'或'phps'),然后是单个管道字符,然后是单个管道字符,然后是JSON(或php serialized serialized数据),然后是PADDADIAD DADA),然后使用PADDing。然后,库可以从其填充物中提取JSON/序列化数据,并照顾其余的解码。
使用PHP JSON_ENCODE()函数将加密输入数据编码为JSON。最初,该库使用php serialize()函数,但显然可以导致某些代码执行方案(我不确定详细信息),因此决定JSON编码更安全。因此,现在,我们使用JSON编码。
将JSON用作数据编码格式的使用对我们可以支持的值有一些较小的含义。特别是我们无法编码以后可以将其解码为对象实例的对象实例(如果对象实现了可序列化的界面,则可以将它们序列化为数据,但是仅将其解码回到PHP数组,而不是它们来自的PHP对象);无法表示一些奇怪的浮点值(即nan,pos inf,neg Info和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编码和解码选项。
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()将其编码为base64,然后对其进行加密。
将来,可能会添加并不总是将JSON编码的数据工作的能力添加到此库中。让我知道这是否是您愿意拥有的功能。
注意:现在使用PHP序列化而不是JSON编码是一个选项;需要更新此文档,以解释其工作原理以及如何使用它。 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 s)之间的延迟。这是针对潜在的计时攻击的缓解措施。有关讨论,请参见S2N和Lucky 13。
请注意,避免正时攻击很难。 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.