API de cifrado muy simple
Necesito un sistema de encriptación con una API súper simple. Preferiría algo tan simple como lo siguiente. La salida también debe ser compatible con el navegador e idealmente compatible con URL.
$encrypted=encrypt(data); $decrypted=decrypt(encrypted);
No soy un criptógrafo, sin embargo, se me ocurrió lo siguiente para lograrlo.
<?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, '-_', '+/'));
}
}
Uso de ejemplo:
$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' => '',
)
Respuestas
Eliminar encryptEasyy decryptEasy; ambas funciones son inseguras y solo sirven para confundir a los posibles usuarios.
Su esquema actual no usa una MAC, ni una suma de verificación, lo que significa que el texto cifrado no está autenticado. Esta es una mala idea . Se recomienda utilizar una función HMAC para la autenticación, con el fin de evitar también el riesgo de ataques de extensión hash mientras se mantiene un rango de salida grande.
Se recomienda cifrar-luego-MAC , mientras que su enfoque actual es hash-luego-encriptar, que en teoría es vulnerable a los ataques de volteo de bits de CTR.
Su implementación de relleno es incorrecta si los datos ya contienen bytes nulos (use PKCS # 7).
En lugar de usar un parámetro de referencia para las excepciones, simplemente genere excepciones directamente.