API mã hóa rất đơn giản
Tôi cần một hệ thống mã hóa với một API siêu đơn giản. Tôi muốn một cái gì đó đơn giản như sau. Đầu ra cũng phải thân thiện với trình duyệt và lý tưởng là thân thiện với url.
$encrypted=encrypt(data); $decrypted=decrypt(encrypted);
Tôi không phải là một nhà mật mã học, tuy nhiên tôi đã nghĩ ra những điều sau để đạt được điều này.
<?php
declare(strict_types = 1);
class EasyCrypt
{
// format: easycryptstart_version_(bytes_of_b64bin)_b64bin_easycryptend
// b64bin: base64urlencode(bin)
// bin: versionspecific
// V1 versionspecific: IV+aes128ctr(encryption_key=hkey,csum+inner_length+data+padding)
// V1 hkey: substr(sha256(key),16); // sha256 is used as a key compressor/expander
// V1 inner_length: little_endian_uint64(strlen(data))
// V1 csum: substr(sha256(inner_length+data+padding),14)
// V1 padding: null-bytes until strlen(csum+inner_length+data+padding) is divisible by 16 bytes (128 bits), (16-(size%16))%16
// generate secure key: cat /dev/urandom | head --bytes=15 | base64
private const EASY_ENCRYPTION_KEY = "CHANGEME";
private const V1_IV_LENGTH = 16;
private const V1_ENCRYPT_ALGO = 'aes-128-ctr';
private const V1_HASH_ALGO = 'sha256';
private const V1_HASH_TRUNCATE_LENGTH = 14;
public static function encryptEasy(string $data): string { return self::encrypt($data, self::EASY_ENCRYPTION_KEY);
}
public static function decryptEasy(string $data, string &$decryptionError = null): ?string
{
return self::decrypt($data, self::EASY_ENCRYPTION_KEY, $decryptionError);
}
public static function encrypt(string $data, string $encryption_key): string
{
$version = 1; $prefix = "easycryptstart_{$version}_"; $postfix = "_easycryptend";
$ret = self::encryptV1($data, $encryption_key); $ret = self::base64url_encode($ret); $ret = $prefix . strlen($ret) . "_" . $ret . $postfix;
return $ret; } public static function decrypt(string $data, string $encryption_key, string &$decryptionError = null): ?string
{
// only 1 "version" exist thus far
$version = 1; $data = str_replace(array(
" ",
"\r",
"\n",
"\t"
), "", $data); $prefix = "easycryptstart_{$version}_"; $postfix = "_easycryptend";
$prefixpos = strpos($data, $prefix); if (false === $prefixpos) {
$decryptionError = "prefix not found"; return null; } $postfixpos = strpos($data, $postfix, $prefixpos); if (false === $postfixpos) {
$decryptionError = "postfix not found (even tho prefix was found!)"; return null; } $data = substr($data, $prefixpos + strlen($prefix), $postfixpos - ($prefixpos + strlen($prefix)));
$outer_length_end = strpos($data, "_");
if (false === $outer_length_end) { $decryptionError = "corrupted input, outer length end missing!";
return null;
}
$outer_length = substr($data, 0, $outer_length_end); $outer_length = filter_var($outer_length, FILTER_VALIDATE_INT); if (false === $outer_length) {
$decryptionError = "corrupt input, outer_length non-int!"; return null; } $data = substr($data, $outer_length_end + strlen("_"));
$dlen = strlen($data);
if ($dlen < $outer_length) {
$decryptionError = "corrupt input, outer length header said {$outer_length} bytes, but only {$dlen} bytes available!"; return null; } $data = substr($data, 0, $outer_length);
$data = self::base64url_decode($data);
return self::decryptV1($data, $encryption_key, $decryptionError); } private static function decryptV1(string $data, string $encryption_key, string &$decryptionError = null): ?string
{
if (strlen($data) < self::V1_IV_LENGTH) { $decryptionError = "corrupt input, IV is missing!";
return null;
}
$IV = substr($data, 0, self::V1_IV_LENGTH);
$data = substr($data, self::V1_IV_LENGTH);
// now we have the aes128 data..
if (strlen($data) < 16 || (strlen($data) % 16) !== 0) {
$decryptionError = "corrupted input, after removing IV, data is not a multiple of 16 bytes!"; return null; } $hkey = hash(self::V1_HASH_ALGO, $encryption_key, true); $hkey = substr($hkey, 0, 16); $data = openssl_decrypt($data, self::V1_ENCRYPT_ALGO, $hkey, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $IV); if (! is_string($data)) {
// should never happen
throw new \RuntimeException("openssl_decrypt failed! wtf!?");
}
if (strlen($data) < self::V1_HASH_TRUNCATE_LENGTH) { $decryptionError = "corrupt input, after decryption, checksum hash is missing!";
return null;
}
$checksum_supplied_hash = substr($data, 0, self::V1_HASH_TRUNCATE_LENGTH);
$data = substr($data, self::V1_HASH_TRUNCATE_LENGTH);
$checksum_calculated_hash = hash(self::V1_HASH_ALGO, $data, true);
$checksum_calculated_hash = substr($checksum_calculated_hash, 0, self::V1_HASH_TRUNCATE_LENGTH);
if (! hash_equals($checksum_calculated_hash, $checksum_supplied_hash)) {
$decryptionError = "checksum mismatch, possibly wrong decryption key?"; return null; } $little_endian_uint64_length = 8;
if (strlen($data) < $little_endian_uint64_length) {
$decryptionError = "after decryption, inner_length header is missing!"; return null; } $little_endian_uint64 = substr($data, 0, $little_endian_uint64_length);
$little_endian_uint64 = self::from_little_uint64_t($little_endian_uint64);
$data = substr($data, $little_endian_uint64_length); $dlen = strlen($data); if ($dlen < $little_endian_uint64) { $decryptionError = "inner_length header said {$little_endian_uint64} bytes, but only {$dlen} bytes remaining, and that includes any padding bytes!";
return null;
}
$data = substr($data, 0, $little_endian_uint64); return $data;
}
private static function encryptV1(string $data, string $encryption_key): string
{
// compress/expand the key so we can accept any encryption key length (instead of the 16 bytes key required by aes-128)
$hkey = hash(self::V1_HASH_ALGO, $encryption_key, true);
$hkey = substr($hkey, 0, 16);
$iv = random_bytes(self::V1_IV_LENGTH); $inner_length_bytes = self::to_little_uint64_t(strlen($data)); $ret = $inner_length_bytes; $ret .= $data; $padding_length = self::V1_HASH_TRUNCATE_LENGTH + strlen($ret); $padding_length = (16 - ($padding_length % 16)) % 16; $ret .= str_repeat("\x00", $padding_length); $csum = hash(self::V1_HASH_ALGO, $ret, true); $csum = substr($csum, 0, self::V1_HASH_TRUNCATE_LENGTH); $ret = $csum . $ret;
$str = openssl_encrypt($ret, self::V1_ENCRYPT_ALGO, $hkey, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $iv);
if (! is_string($str)) { // should never happen throw new \RuntimeException("openssl_encrypt failed! wtf!?"); } $str = $iv . $str;
return $str; } private static function to_uint8_t(int $i): string
{
return pack('C', $i); } private static function from_uint8_t(string $i): int
{
// ord($i) , i know. $arr = unpack("Cuint8_t", $i); return $arr['uint8_t'];
}
private static function to_little_uint64_t(int $i): string { return pack('P', $i);
}
private static function from_little_uint64_t(string $i): int { $arr = unpack('Puint64_t', $i); return $arr['uint64_t'];
}
private static function base64url_encode($data) { return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
private static function base64url_decode($data) { return base64_decode(strtr($data, '-_', '+/'));
}
}
Ví dụ sử dụng:
$data = "Hello World!"; // . random_bytes(10*1024*1024); $decryptionError = "";
$encrypted = EasyCrypt::encryptEasy($data);
$decrypted = EasyCrypt::decryptEasy($encrypted, $decryptionError); $pretty = [
"data to encrypt" => $data, "encrypted" => $encrypted,
"decrypted successfully" => $decrypted === $data,
"decryption error" => $decryptionError ]; var_export($pretty);
$ php EasyCrypt.php | more
array (
'data to encrypt' => 'Hello World!',
'encrypted' => 'easycryptstart_1_86_LvBV6n3yLY-sH3vdhjzIZmbAm56s7VEZ9ah0wh5z4p9-rhJBaIDmOQYaWOTuRSei7yfmXJ6HTbqgvBaQJsQdMg_easycryptend',
'decrypted successfully' => true,
'decryption error' => '',
)
Trả lời
Loại bỏ encryptEasyvà decryptEasy; cả hai chức năng đều không an toàn và chỉ nhằm mục đích gây nhầm lẫn cho người dùng có thể.
Lược đồ hiện tại của bạn không sử dụng MAC, không sử dụng tổng kiểm tra có nghĩa là bản mã chưa được xác thực. Đây là một ý tưởng tồi . Bạn nên sử dụng hàm HMAC để xác thực, để tránh rủi ro bị tấn công mở rộng băm trong khi vẫn giữ phạm vi đầu ra lớn.
Mã hóa-sau đó-MAC được khuyến nghị , trong khi cách tiếp cận hiện tại của bạn là mã hóa băm-sau đó, về mặt lý thuyết dễ bị tấn công bitflipping CTR.
Việc triển khai phần đệm của bạn không chính xác nếu dữ liệu đã chứa byte rỗng (Sử dụng PKCS # 7).
Thay vì sử dụng một tham số tham chiếu cho các ngoại lệ, chỉ cần ném trực tiếp các ngoại lệ.