Skip to content
Permalink
 
 
Cannot retrieve contributors at this time
271 lines (229 sloc) 6.55 KB
<?php
declare(strict_types=1);
namespace Leevel\Encryption;
use InvalidArgumentException;
use Throwable;
/**
* 加密组件.
*/
class Encryption implements IEncryption
{
/**
* 加密 key.
*/
protected string $key;
/**
* openssl 加密解密算法.
*/
protected string $cipher;
/**
* 安全 RSA 私钥.
*/
protected ?string $rsaPrivate = null;
/**
* 安全 RSA 公钥.
*/
protected ?string $rsaPublic = null;
/**
* 构造函数.
*/
public function __construct(string $key, string $cipher = 'AES-256-CBC', ?string $rsaPrivate = null, ?string $rsaPublic = null)
{
$this->validateCipher($cipher);
$this->key = $key;
$this->cipher = $cipher;
$this->rsaPrivate = $rsaPrivate;
$this->rsaPublic = $rsaPublic;
}
/**
* {@inheritDoc}
*/
public function encrypt(string $value, int $expiry = 0): string
{
$iv = $this->createIv();
$value = $this->packData($value, $expiry, $iv);
return $this->encryptData($value, $iv);
}
/**
* {@inheritDoc}
*/
public function decrypt(string $value): string
{
if (false === ($value = $this->decryptData($value))) {
return '';
}
list($data, $iv) = $value;
return $this->validateData($data, $iv);
}
/**
* 打包数据.
*/
protected function packData(string $value, int $expiry, string $iv): string
{
$data = [
$this->normalizeExpiry($expiry),
base64_encode($value),
base64_encode($iv),
$this->normalizeSign($value),
];
return implode("\t", $data);
}
/**
* 解包数据.
*/
protected function unpackData(string $value): array|bool
{
$data = explode("\t", $value);
if (4 !== count($data)) {
return false;
}
$key = ['expiry', 'value', 'iv', 'sign'];
return array_combine($key, $data);
}
/**
* 加密数据.
*
* @throws \InvalidArgumentException
*/
protected function encryptData(string $value, string $iv): string
{
$value = openssl_encrypt($value, $this->cipher, $this->key, OPENSSL_RAW_DATA, $iv);
if (false === $value) {
// 在 error_reporting(0) 场景下 openssl 加密解密算法 cipher 错误的情况下才会执行
throw new InvalidArgumentException('Encrypt the data failed.');
}
return $this->packDataWithIv($value, $iv);
}
/**
* 解密数据.
*
* @throws \InvalidArgumentException
*/
protected function decryptData(string $value): array|bool
{
if (false === ($value = base64_decode($value, true))) {
return false;
}
if (false === ($value = $this->unpackDataWithIv($value))) {
return false;
}
$data = openssl_decrypt(
$value['value'],
$this->cipher,
$this->key,
OPENSSL_RAW_DATA,
$value['iv']
);
if (false === $data) {
throw new InvalidArgumentException('Decrypt the data failed.');
}
return [$data, base64_encode($value['iv'])];
}
/**
* 数据加入向量并打包.
*/
protected function packDataWithIv(string $value, string $iv): string
{
return base64_encode(base64_encode($value)."\t".base64_encode($iv));
}
/**
* 解包带向量的数据.
*/
protected function unpackDataWithIv(string $value): array|bool
{
$data = explode("\t", $value);
if (2 !== count($data)) {
return false;
}
$key = ['value', 'iv'];
$data[0] = base64_decode($data[0], true);
$data[1] = base64_decode($data[1], true);
return array_combine($key, $data);
}
/**
* 格式化过期时间.
*/
protected function normalizeExpiry(int $expiry = 0): string
{
return sprintf('%010d', $expiry ? $expiry + time() : 0);
}
/**
* 创建初始化向量.
*/
protected function createIv(): string
{
return openssl_random_pseudo_bytes(openssl_cipher_iv_length($this->cipher));
}
/**
* 获取签名.
*
* @throws \InvalidArgumentException
*/
protected function normalizeSign(string $value): string
{
if (!$this->rsaPrivate) {
return '';
}
try {
$rsaPrivate = openssl_pkey_get_private($this->rsaPrivate);
if (openssl_sign($value, $sign, $rsaPrivate)) {
return base64_encode($sign);
}
// 在 error_reporting(0) 场景下签名 $rsaPrivate 错误的情况下才会执行
throw new InvalidArgumentException('Openssl sign failed.');
} catch (Throwable $e) {
throw new InvalidArgumentException($e->getMessage());
}
}
/**
* 校验加密算法.
*
* @throws \InvalidArgumentException
*/
protected function validateCipher(string $cipher): void
{
if (!in_array(strtolower($cipher), openssl_get_cipher_methods(), true)) {
$e = sprintf('Encrypt cipher `%s` was not found.', $cipher);
throw new InvalidArgumentException($e);
}
}
/**
* 校验数据正确性.
*/
protected function validateData(string $data, string $iv): string
{
if (false === ($data = $this->unpackData($data))) {
return '';
}
if ($data['iv'] !== $iv ||
('0000000000' !== $data['expiry'] && time() > $data['expiry'])) {
return '';
}
$result = base64_decode($data['value'], true) ?: false;
if (false === $result) {
return '';
}
return $this->validateSign($result, $data['sign']);
}
/**
* 验证签名.
*
* @throws \InvalidArgumentException
*/
protected function validateSign(string $value, string $sign): string
{
if (!$this->rsaPublic) {
return $value;
}
try {
$rsaPublic = openssl_pkey_get_public($this->rsaPublic);
if (1 === openssl_verify($value, base64_decode($sign, true), $rsaPublic)) {
return $value;
}
// 在 error_reporting(0) 场景下签名 $rsaPublic 错误的情况下才会执行
throw new InvalidArgumentException('Openssl verify sign failed.');
} catch (Throwable $e) {
throw new InvalidArgumentException($e->getMessage());
}
}
}