API de chiffrement très simple

Sep 09 2020

J'ai besoin d'un système de cryptage avec une API super simple. Je préférerais quelque chose d'aussi simple que ce qui suit. La sortie doit également être compatible avec le navigateur et idéalement pour les URL.

$encrypted=encrypt(data); $decrypted=decrypt(encrypted);

Je ne suis pas un cryptographe, mais j'ai mis au point ce qui suit pour y parvenir.

<?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, '-_', '+/'));
    }
}

Exemple d'utilisation:

$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' => '',
)

Réponses

4 SuperStormer Sep 09 2020 at 08:21

Supprimer encryptEasyet decryptEasy; les deux fonctions ne sont pas sûres et ne servent qu'à confondre les utilisateurs potentiels.

Votre schéma actuel n'utilise pas de MAC, pas de somme de contrôle, ce qui signifie que le texte chiffré n'est pas authentifié. C'est une mauvaise idée . Il est recommandé d'utiliser une fonction HMAC pour l'authentification, afin d'éviter également le risque d'attaques d'extension de hachage tout en gardant la plage de sortie large.

Encrypt-then-MAC est recommandé , tandis que votre approche actuelle est le hash-then-encrypt qui est théoriquement vulnérable aux attaques de bitflipping CTR.

Votre implémentation de remplissage est incorrecte si les données contiennent déjà des octets nuls (utilisez PKCS # 7).

Au lieu d'utiliser un paramètre de référence pour les exceptions, lancez simplement les exceptions directement.