lekoala / silverstripe-encrypt
使用CipherSweet加密SilverStripe的数据库字段和文件
Requires
- php: ^8.1
- paragonie/ciphersweet: ^4
- silverstripe/assets: ^2
- silverstripe/framework: ^5
- silverstripe/recipe-plugin: ^2
- silverstripe/vendor-plugin: ^2
Requires (Dev)
- phpunit/phpunit: ^9.5
- silverstripe/admin: ^2
- silverstripe/versioned: ^2
- squizlabs/php_codesniffer: ^3.5
README
轻松为您的DataObjects添加加密功能。在GDPR和数据泄露的时代,此模块帮助您保护数据安全。
此模块底层使用ciphersweet来加密字段数据
感谢CipherSweet,您的加密数据可搜索!
注意:本模块当前版本与分支2的复合字段不兼容。请计划轮换您的值或继续使用旧版本。
注意:本模块的分支2与旧版本不兼容。如果您需要使用之前的加密系统,请使用分支1。
如何使用
首先,您需要将加密密钥定义为您环境的一部分。您可以在.env
文件中这样做
ENCRYPTION_KEY='here_is_my_key'
您可以使用EncryptHelper::generateKey()
生成密钥。
确保您的密钥安全,并且没有人能够访问它
此模块的工作原理
您定义加密字段类型。默认情况下,所有内容都以文本格式(变长文本或文本)存储。这更简单,因为我们的加密数据是文本格式。
class MySecureObject extends DataObject { use HasEncryptedFields; private static $db = [ "Name" => 'Varchar', "MyText" => EncryptedDBText::class, "MyHTMLText" => EncryptedDBHTMLText::class, "MyVarchar" => EncryptedDBVarchar::class, "MyNumber" => EncryptedNumberField::class, "MyIndexedVarchar" => EncryptedDBField::class, ]; private static $indexes = [ 'MyIndexedVarcharBlindIndex' => true, 'MyNumberBlindIndex' => true, 'MyNumberLastFourBlindIndex' => true, ]; public function getField($field) { return $this->getEncryptedField($field); } public function setField($fieldName, $val) { return $this->setEncryptedField($fieldName, $val); } }
有两种类型的字段:简单字段和索引(基于复合字段)。
在调用write
之前对值进行编码,在调用getField
(或任何__get)时进行解码。这就是为什么我们必须使用HasEncryptedFields特性和透明地编码和解码数据。否则,如果未使用dbObject调用,最终会从数据库中加载加密数据,但永远不会解码。
当然,您可以使用特性和透明地编码和解码数据。否则,请记住,您的调用$myObject->myEncryptedField = 'my value'不会自动编码。但您绝对可以这样做$myObject->dbObject('myEncryptedField')->setValue('my value') ... 但我认为这真的不方便。也许我会找到一种避免覆盖get/set字段方法的方法,但到目前为止还没有成功。
关于索引的简要说明
请注意,此模块不会自动为您创建盲索引的索引。由于您很可能会使用它们来搜索记录,因此添加数据库索引以避免全表扫描是一个好主意。
注意:盲索引可能有假阳性(两条记录具有相同的索引),因此您不能确定给定的盲索引只会返回一条记录。
EncryptHelper::planIndexSizeForClass
函数将帮助您设置正确的值。它返回一个类似于以下内容的数组
Array ( [min] => 2 [max] => 32 [indexes] => 2 [coincidence_count] => 8589934592 [coincidence_ratio] => 9.3132257461548E-8 [estimated_population] => 9223372036854775807 )
对于每个加密类,您可以设置以下配置值
- estimated_population:预期的记录数。人口越多,巧合计数就越高(这使得盲索引安全使用)
- output_size和domain_size:这些设置在字段级别配置。
private static $db = [ "MyNumber" => EncryptedNumberField::class . '(["output_size" => 4, "domain_size" => 10, "index_size" => 32])', ];
快速哈希
默认情况下,此模块不启用快速哈希索引。如果您预计将对大表进行大量查询,则需要启用它。 确保在#encrypt之后运行,以覆盖模块中的yml配置。
--- After: - '#encrypt' --- LeKoala\Encrypt\EncryptHelper: fasthash: true
这是一个全局设置。快速哈希与慢速哈希不同,因此如果有现有数据,您需要迁移它。如果需要,您可以使用EncryptHelper::convertHashType
来帮助您。
简单字段类型
本模块提供了三个不带盲索引的字段(如果需要盲索引,请参阅下一点)
- EncryptedDBText
- EncryptedDBVarchar
- EncryptedDBHTMLText
这些字段的工作方式与它们的常规对应字段完全相同,只是数据被加密了。
JSON数据类型
使用EncryptedDBJson可以存储json数据。默认情况下,它将加密整个json表示形式,但这将阻止使用现代数据库引擎的功能来访问特定的键。
相反,您可以像这样加密json数据的每个部分
// create definition somewhere... $map = (new JsonFieldMap()) ->addTextField('name') ->addBooleanField('active') ->addIntegerField('age'); $definition = EncryptHelper::convertJsonMapToDefinition($map); // in your models... private static $db = [ "MyEncryptedJson" => EncryptedDBJson::class . "(['map' => '7551830f{\"fields\":{\"$6e616d65\":\"string\",\"$616374697665\":\"bool\",\"$616765\":\"int\"}}'])", ];
映射需要以字符串表示形式存储在字段定义中的映射选项下。可以使用EncryptHelper::convertJsonMapToDefinition
创建。
注意:未指定的键将保持未加密状态。
搜索数据
多亏了CipherSweet,数据使用盲索引进行加密。如果您知道基于您创建的索引类型的数据值或部分值,可以使用此盲索引来搜索数据。
要使用索引进行搜索,请使用EncryptedDBField实例
$singl = singleton(MyModel::class); $obj = $singl->dbObject('MyEncryptedField'); $searchValue = $obj->getSearchValue($value); $query = MyModel::get()->where(array('MyEncryptedFieldBlindIndex = ?' => $searchValue));
或者使用快捷方式
$singl = singleton(MyModel::class); $obj = $singl->dbObject('MyEncryptedField'); $record = $obj->fetchRecord($value);
或者更好的是
$record = MyModel::getByBlindIndex("MyEncryptedField", $value); $list = MyModel::getAllByBlindIndex("MyEncryptedField", $value);
由于假阳性的存在,我们无法使用常规的搜索过滤器。
强烈建议在您的使用盲索引的字段上设置索引。惯例如下:{Name}BlindIndex和{Name}LastFourBlindIndex
本模块提供了两个带有盲索引的字段
- EncryptedDBField,它包含单个值并具有完整的盲索引
- EncryptedNumberField,它包含单个值并具有完整的盲索引和最后四位数的盲索引
您可以通过添加以"Encrypted"开头的名称来扩展EncryptedDBField
,以添加更多字段类型以适应您的用例。
加密和解密其他类型的数据
您还可以使用助手使用对称密钥加密和解密数据
$someText = 'some text'; $encrypt = EncryptHelper::encrypt($someText); $decryptedValue = EncryptHelper::decrypt($encrypt);
处理加密文件
此模块自动将EncryptedDBFile
扩展添加到您的文件中。这是通过扩展基本File类来完成的,以避免在表中添加一个新表,以便添加一个跟踪加密状态的Encrypted
字段
请注意,文件默认情况下不会被加密,您需要在上传后调用encryptFileIfNeeded
。
$myFile->encryptFileIfNeeded();
或者使用EncryptedFile
类。最好使用EncryptedFile
类,因为它会在更新文件等操作时正确更新加密标志。相比于使用isEncrypted
,建议检查加密标志,因为此方法较慢。
另外,从性能的角度来看,记住加载文件以检查其状态可能会很慢
$file = $this->File(); $file->encryptFileIfNeeded(); // fine for one record, not fine in a loop! Use EncryptHelper::checkIfFileIsEncrypted with ID
注意:即使您的文件已加密,它们也不应在您的公共文件夹中可用。
请确保审查SilverStripe文件安全文档。保持文件为.protected并通过专用控制器(使用sendDecryptedFile
)提供,或者通过DecryptController
是必要的。
密钥轮换
如果您需要更改算法或密钥,您将需要轮换加密。
使用相同密钥轮换算法很容易,并且内置在此模块中。默认情况下,它会自动发生,您可以使用needsToRotateEncryption
和rotateEncryption
方法。
如果您需要更改密钥,您需要首先在环境变量中引用它
OLD_ENCRYPTION_KEY='here_is_my_old_key'
然后像这样调用rotateEncryption
$oldKey = EncryptHelper::getOldKey(); $old = EncryptHelper::getEngineForEncryption("nacl", $oldKey); $result = $model->needsToRotateEncryption($old); if($result) { $result = $model->rotateEncryption($old); }
规划索引大小
如果您使用盲索引,您可能需要规划它们的大小。
强烈建议阅读以下关于盲索引规划的指南。https://ciphersweet.paragonie.com/php/blind-index-planning
此模块为您提供了工具和默认设置,可以帮助您正确配置索引。
默认情况下,盲索引的大小为32个字符,这允许在表中具有大量记录,但具有非常低的
使用aad
默认情况下,此模块将使用AAD。
这会将密文绑定到特定行,从而防止攻击者替换密文并使用合法的应用程序访问来解密他们原本无法访问的密文。
此设置由 aad_source
参数控制,默认值为 "ID"。您可以通过将其设置为空字符串来禁用 aad。
在首次写入时,记录会获取其 ID。因此,您需要确保在加密字段上设置了 ID。可以这样做。如果未这样做,也有一些安全检查。
protected function writeBaseRecord($baseTable, $now) { parent::writeBaseRecord($baseTable, $now); // After base record is written, we have an ID and therefore AAD has changed $this->resetFieldValues(); }
待办事项
- 为成员加密电子邮件字段的方法
- 从外部服务获取密钥并缓存
兼容性
已测试与 SilverStripe 5+
维护者
LeKoala - thomas@lekoala.be