Sehr einfache Verschlüsselungs-API

Sep 09 2020

Ich brauche ein Verschlüsselungssystem mit einer super einfachen API. Ich würde etwas so Einfaches wie das Folgende bevorzugen. Die Ausgabe sollte auch browserfreundlich und idealerweise url-freundlich sein.

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

Ich bin kein Kryptograf, habe mir jedoch Folgendes ausgedacht, um dies zu erreichen.

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

Anwendungsbeispiel:

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

Antworten

4 SuperStormer Sep 09 2020 at 08:21

Entfernen Sie encryptEasyund decryptEasy; Beide Funktionen sind unsicher und dienen nur dazu, mögliche Benutzer zu verwirren.

Ihr aktuelles Schema verwendet keinen MAC, keine Prüfsumme, was bedeutet, dass der Chiffretext nicht authentifiziert ist. Das ist eine schlechte Idee . Es wird empfohlen, eine HMAC-Funktion zur Authentifizierung zu verwenden, um das Risiko von Hash-Erweiterungsangriffen zu vermeiden und gleichzeitig den Ausgabebereich groß zu halten.

Encrypt-then-MAC wird empfohlen , während Ihr aktueller Ansatz Hash-then-encrypt ist, das theoretisch anfällig für CTR-Bitflipping-Angriffe ist.

Ihre Padding-Implementierung ist falsch, wenn die Daten bereits Null-Bytes enthalten (PKCS # 7 verwenden).

Anstatt einen Referenzparameter für Ausnahmen zu verwenden, werfen Sie einfach Ausnahmen direkt aus.