Najprostsze dwukierunkowe szyfrowanie przy użyciu PHP

230

Jaki jest najprostszy sposób szyfrowania dwukierunkowego w typowych instalacjach PHP?

Muszę być w stanie zaszyfrować dane za pomocą klucza łańcuchowego i użyć tego samego klucza do odszyfrowania na drugim końcu.

Bezpieczeństwo nie jest tak dużym problemem, jak przenośność kodu, dlatego chciałbym mieć prostotę. Obecnie używam implementacji RC4, ale jeśli mogę znaleźć coś natywnie obsługiwanego, sądzę, że mogę zaoszczędzić wiele niepotrzebnego kodu.

użytkownik1206970
źródło
3
Do szyfrowania ogólnego zastosowania użyj defuse / php-encryption / zamiast zwijania własnego.
Scott Arciszewski,
2
Ręce precz od github.com/defuse/php-encryption - jest wolniejszy o rząd wielkości niż mcrypt.
Eugen Rieck
1
@ Scott Myślenie w stylu „to prawdopodobnie nie będzie wąskim gardłem” przyniosło nam wiele złego oprogramowania.
Eugen Rieck,
3
Jeśli naprawdę szyfrujesz / odszyfrowujesz wiele danych do tego stopnia, że ​​milisekundy, które to kosztuje, zapełniają twoją aplikację, ugryź pocisk i przełącz się na libsodium. Sodium::crypto_secretbox()i Sodium::crypto_secretbox_open()są bezpieczne i wydajne.
Scott Arciszewski

Odpowiedzi:

196

Edytowane:

Naprawdę powinieneś używać openssl_encrypt () i openssl_decrypt ()

Jak mówi Scott , Mcrypt nie jest dobrym pomysłem, ponieważ nie był aktualizowany od 2007 roku.

Istnieje nawet RFC do usunięcia Mcrypta z PHP - https://wiki.php.net/rfc/mcrypt-viking-funeral

472084
źródło
6
@EugenRieck Tak, o to chodzi. Mcrypt nie otrzymuje łatek. OpenSSL otrzymuje łaty, gdy tylko zostanie wykryta luka, duża lub mała.
Greg
5
byłoby lepiej, gdyby tak wysoko głosowana odpowiedź zawierała również najprostsze przykłady w odpowiedzi. w każdym razie dzięki.
T.Todua
chłopaki, po prostu FYI => MCRYPT JEST DEPRECATED. ograniczanie, więc każdy powinien wiedzieć, aby go nie używać, ponieważ dostarczyło nam to niezliczonych problemów. Jest przestarzałe od PHP 7.1, jeśli się nie mylę.
clusterBuddy
Od PHP 7 funkcja mcrypt jest usuwana z bazy kodu php. Więc używając najnowszej wersji php (która powinna być standardowa), nie możesz już używać tej przestarzałej funkcji.
Alexander Behling
234

Ważne : Jeżeli nie masz bardzo szczególne zastosowanie przypadku, Nie należy szyfrować hasła , należy użyć algorytmu mieszającego hasło. Kiedy ktoś mówi, że szyfruje swoje hasło w aplikacji po stronie serwera, jest albo niedoinformowany, albo opisuje niebezpieczny projekt systemu. Bezpieczne przechowywanie haseł to zupełnie inny problem niż szyfrowanie.

Być poinformowanym. Projektuj bezpieczne systemy.

Przenośne szyfrowanie danych w PHP

Jeśli używasz PHP 5.4 lub nowszego i nie chcesz samodzielnie pisać modułu kryptograficznego, zalecamy użycie istniejącej biblioteki, która zapewnia uwierzytelnianie szyfrowane . Biblioteka, którą podłączyłem, opiera się tylko na tym, co zapewnia PHP i jest okresowo sprawdzana przez garść badaczy bezpieczeństwa. (Ja w tym.)

Jeśli twoje cele w zakresie przenoszenia nie zapobiegają wymaganiu rozszerzeń PECL, libsodium jest wysoce zalecane w stosunku do wszystkiego, co możesz napisać w PHP.

Aktualizacja (2016-06-12): Możesz teraz używać sodu_kompat i korzystać z tych samych ofert krypto-libsodium bez instalowania rozszerzeń PECL.

Jeśli chcesz spróbować swoich sił w inżynierii kryptograficznej, czytaj dalej.


Po pierwsze, powinieneś poświęcić czas na poznanie niebezpieczeństw związanych z nieuwierzytelnionym szyfrowaniem i zasadą Cryptographic Doom .

  • Zaszyfrowane dane mogą być nadal modyfikowane przez złośliwego użytkownika.
  • Uwierzytelnianie zaszyfrowanych danych zapobiega manipulowaniu.
  • Uwierzytelnianie niezaszyfrowanych danych nie zapobiega manipulacji.

Szyfrowanie i deszyfrowanie

Szyfrowanie w PHP jest faktycznie proste (będziemy używać openssl_encrypt()i openssl_decrypt()po dokonaniu pewnych decyzji dotyczących sposobu szyfrowania informacji Consult. openssl_get_cipher_methods()O listę obsługiwanych metod w systemie Najlepszym wyborem jest. AES w trybie CTR :

  • aes-128-ctr
  • aes-192-ctr
  • aes-256-ctr

Obecnie nie ma powodu, aby sądzić, że rozmiar klucza AES jest poważnym problemem, o który należy się martwić (prawdopodobnie większy nie jest lepszy, z powodu złego planowania klucza w trybie 256-bitowym).

Uwaga: nie korzystamy z mcryptniego, ponieważ jest to porzucone oprogramowanie i zawiera niezałatane błędy, które mogą mieć wpływ na bezpieczeństwo. Z tych powodów zachęcam również innych programistów PHP, aby tego unikali.

Proste opakowanie szyfrowania / deszyfrowania za pomocą OpenSSL

class UnsafeCrypto
{
    const METHOD = 'aes-256-ctr';

    /**
     * Encrypts (but does not authenticate) a message
     * 
     * @param string $message - plaintext message
     * @param string $key - encryption key (raw binary expected)
     * @param boolean $encode - set to TRUE to return a base64-encoded 
     * @return string (raw binary)
     */
    public static function encrypt($message, $key, $encode = false)
    {
        $nonceSize = openssl_cipher_iv_length(self::METHOD);
        $nonce = openssl_random_pseudo_bytes($nonceSize);

        $ciphertext = openssl_encrypt(
            $message,
            self::METHOD,
            $key,
            OPENSSL_RAW_DATA,
            $nonce
        );

        // Now let's pack the IV and the ciphertext together
        // Naively, we can just concatenate
        if ($encode) {
            return base64_encode($nonce.$ciphertext);
        }
        return $nonce.$ciphertext;
    }

    /**
     * Decrypts (but does not verify) a message
     * 
     * @param string $message - ciphertext message
     * @param string $key - encryption key (raw binary expected)
     * @param boolean $encoded - are we expecting an encoded string?
     * @return string
     */
    public static function decrypt($message, $key, $encoded = false)
    {
        if ($encoded) {
            $message = base64_decode($message, true);
            if ($message === false) {
                throw new Exception('Encryption failure');
            }
        }

        $nonceSize = openssl_cipher_iv_length(self::METHOD);
        $nonce = mb_substr($message, 0, $nonceSize, '8bit');
        $ciphertext = mb_substr($message, $nonceSize, null, '8bit');

        $plaintext = openssl_decrypt(
            $ciphertext,
            self::METHOD,
            $key,
            OPENSSL_RAW_DATA,
            $nonce
        );

        return $plaintext;
    }
}

Przykład użycia

$message = 'Ready your ammunition; we attack at dawn.';
$key = hex2bin('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f');

$encrypted = UnsafeCrypto::encrypt($message, $key);
$decrypted = UnsafeCrypto::decrypt($encrypted, $key);

var_dump($encrypted, $decrypted);

Demo : https://3v4l.org/jl7qR


Powyższa prosta biblioteka kryptograficzna nadal nie jest bezpieczna w użyciu. Musimy uwierzytelnić teksty zaszyfrowane i zweryfikować je przed odszyfrowaniem .

Uwaga : Domyślnie UnsafeCrypto::encrypt()zwraca surowy ciąg binarny. Nazwij to tak, jeśli chcesz przechowywać go w formacie binarnie bezpiecznym (zakodowanym w base64):

$message = 'Ready your ammunition; we attack at dawn.';
$key = hex2bin('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f');

$encrypted = UnsafeCrypto::encrypt($message, $key, true);
$decrypted = UnsafeCrypto::decrypt($encrypted, $key, true);

var_dump($encrypted, $decrypted);

Demo : http://3v4l.org/f5K93

Proste opakowanie uwierzytelnienia

class SaferCrypto extends UnsafeCrypto
{
    const HASH_ALGO = 'sha256';

    /**
     * Encrypts then MACs a message
     * 
     * @param string $message - plaintext message
     * @param string $key - encryption key (raw binary expected)
     * @param boolean $encode - set to TRUE to return a base64-encoded string
     * @return string (raw binary)
     */
    public static function encrypt($message, $key, $encode = false)
    {
        list($encKey, $authKey) = self::splitKeys($key);

        // Pass to UnsafeCrypto::encrypt
        $ciphertext = parent::encrypt($message, $encKey);

        // Calculate a MAC of the IV and ciphertext
        $mac = hash_hmac(self::HASH_ALGO, $ciphertext, $authKey, true);

        if ($encode) {
            return base64_encode($mac.$ciphertext);
        }
        // Prepend MAC to the ciphertext and return to caller
        return $mac.$ciphertext;
    }

    /**
     * Decrypts a message (after verifying integrity)
     * 
     * @param string $message - ciphertext message
     * @param string $key - encryption key (raw binary expected)
     * @param boolean $encoded - are we expecting an encoded string?
     * @return string (raw binary)
     */
    public static function decrypt($message, $key, $encoded = false)
    {
        list($encKey, $authKey) = self::splitKeys($key);
        if ($encoded) {
            $message = base64_decode($message, true);
            if ($message === false) {
                throw new Exception('Encryption failure');
            }
        }

        // Hash Size -- in case HASH_ALGO is changed
        $hs = mb_strlen(hash(self::HASH_ALGO, '', true), '8bit');
        $mac = mb_substr($message, 0, $hs, '8bit');

        $ciphertext = mb_substr($message, $hs, null, '8bit');

        $calculated = hash_hmac(
            self::HASH_ALGO,
            $ciphertext,
            $authKey,
            true
        );

        if (!self::hashEquals($mac, $calculated)) {
            throw new Exception('Encryption failure');
        }

        // Pass to UnsafeCrypto::decrypt
        $plaintext = parent::decrypt($ciphertext, $encKey);

        return $plaintext;
    }

    /**
     * Splits a key into two separate keys; one for encryption
     * and the other for authenticaiton
     * 
     * @param string $masterKey (raw binary)
     * @return array (two raw binary strings)
     */
    protected static function splitKeys($masterKey)
    {
        // You really want to implement HKDF here instead!
        return [
            hash_hmac(self::HASH_ALGO, 'ENCRYPTION', $masterKey, true),
            hash_hmac(self::HASH_ALGO, 'AUTHENTICATION', $masterKey, true)
        ];
    }

    /**
     * Compare two strings without leaking timing information
     * 
     * @param string $a
     * @param string $b
     * @ref https://paragonie.com/b/WS1DLx6BnpsdaVQW
     * @return boolean
     */
    protected static function hashEquals($a, $b)
    {
        if (function_exists('hash_equals')) {
            return hash_equals($a, $b);
        }
        $nonce = openssl_random_pseudo_bytes(32);
        return hash_hmac(self::HASH_ALGO, $a, $nonce) === hash_hmac(self::HASH_ALGO, $b, $nonce);
    }
}

Przykład użycia

$message = 'Ready your ammunition; we attack at dawn.';
$key = hex2bin('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f');

$encrypted = SaferCrypto::encrypt($message, $key);
$decrypted = SaferCrypto::decrypt($encrypted, $key);

var_dump($encrypted, $decrypted);

Dema : surowe pliki binarne , zakodowane w base64


Jeśli ktoś chce użyć tej SaferCryptobiblioteki w środowisku produkcyjnym lub własnej implementacji tych samych pojęć, zdecydowanie zalecamy skontaktowanie się z lokalnymi kryptografami w celu uzyskania drugiej opinii. Będą mogli powiedzieć ci o błędach, których być może nawet nie będę świadomy.

Lepiej skorzystasz z renomowanej biblioteki kryptograficznej .

Scott Arciszewski
źródło
3
Właśnie próbuję najpierw uruchomić UnsafeCrypto. Szyfrowanie odbywa się dobrze, ale za każdym razem, gdy uruchamiam deszyfrowanie, otrzymuję odpowiedź „fałszywą”. Używam tego samego klucza do odszyfrowywania i przekazuję true zarówno w kodowaniu, jak i w dekodowaniu. W tym przykładzie zakładam literówkę. Zastanawiam się, czy właśnie stąd pochodzi mój problem. Czy możesz wyjaśnić, skąd pochodzi zmienna $ mac i czy powinna to być po prostu $ iv?
David C
1
@EugenRieck Implementacje szyfrów OpenSSL są prawdopodobnie jedynymi częściami, które nie są do bani, i jest to jedyny sposób na wykorzystanie AES-NI w waniliowym PHP. Jeśli zainstalujesz na OpenBSD, PHP zostanie skompilowane z LibreSSL bez zauważenia różnicy w kodzie PHP. Libsodium> OpenSSL każdego dnia. Ponadto, nie należy używać libmcrypt . Czego poleciłbyś programistom PHP zamiast OpenSSL?
Scott Arciszewski
2
Ani 5.2, ani 5.3 nie są już obsługiwane . Zamiast tego powinieneś rozważyć aktualizację do obsługiwanej wersji PHP , takiej jak 5.6.
Scott Arciszewski,
1
@BBeta paragonie.com/blog/2015/09/…
Scott Arciszewski,
1
Zrobiłem to właśnie jako demonstracja twoich kluczy binarnych, a nie łańcuchów czytelnych dla człowieka, dla twoich kluczy .
Scott Arciszewski
22

Użyj mcrypt_encrypt()imcrypt_decrypt() z odpowiednimi parametrami. Naprawdę łatwe i proste, a używasz sprawdzonego w bitwie pakietu szyfrującego.

EDYTOWAĆ

5 lat i 4 miesiące po tej odpowiedzi mcryptrozszerzenie jest obecnie w fazie wycofywania i ostatecznego usunięcia z PHP.

Eugen Rieck
źródło
34
Bitwa przetestowana i nie aktualizowana przez ponad 8 lat?
Maarten Bodewes
2
Cóż, mcrypt jest w PHP7 i nie jest przestarzały - to mi wystarcza. Nie cały kod ma straszną jakość OpenSSL i wymaga łatania co kilka dni.
Eugen Rieck
3
mcrypt jest nie tylko okropny pod względem wsparcia. Nie wdraża również najlepszych praktyk, takich jak wypełnianie zgodne z PKCS # 7, uwierzytelnianie szyfrowane. Nie będzie obsługiwał SHA-3 ani żadnego innego nowego algorytmu, ponieważ nikt go nie utrzymuje, okradając cię ze ścieżki aktualizacji. Ponadto zwykł akceptować takie rzeczy jak częściowe klucze, wypełnianie zerami itp. Jest dobry powód, dla którego jest on stopniowo usuwany z PHP.
Maarten Bodewes
2
W PHP 7.1 wszystkie funkcje mcrypt_ * wywołają powiadomienie E_DEPRECATED. W PHP 7.1 + 1 (7,2 lub 8,0) rozszerzenie mcrypt zostanie przeniesione z rdzenia do PECL, gdzie ludzie, którzy naprawdę chcą go zainstalować, mogą to zrobić, jeśli mogą zainstalować rozszerzenia PHP z PECL.
Mladen Janjetovic
4

PHP 7.2 całkowicie się odsunął, Mcrypta szyfrowanie jest teraz oparte na łatwym do utrzymaniaLibsodium bibliotece, którą można .

Wszystkie potrzeby szyfrowania można w zasadzie rozwiązać za pomocą Libsodiumbiblioteki.

// On Alice's computer:
$msg = 'This comes from Alice.';
$signed_msg = sodium_crypto_sign($msg, $secret_sign_key);


// On Bob's computer:
$original_msg = sodium_crypto_sign_open($signed_msg, $alice_sign_publickey);
if ($original_msg === false) {
    throw new Exception('Invalid signature');
} else {
    echo $original_msg; // Displays "This comes from Alice."
}

Dokumentacja Libsodium: https://github.com/paragonie/pecl-libsodium-doc

Hemerson Varela
źródło
2
jeśli wkleisz trochę kodu, upewnij się, że wszystkie zmienne są uwzględnione. W twoim przykładzie $ secret_sign_key i $ alice_sign_publickey mają wartość NULL
undefinedman
1
crypto_signAPI czy nie szyfrować wiadomości - że wymagać będzie jedną z crypto_aead_*_encryptfunkcji.
Roger Dueck
1

WAŻNY ta odpowiedź jest ważna tylko dla PHP 5, w PHP 7 używaj wbudowanych funkcji kryptograficznych.

Oto prosta, ale wystarczająco bezpieczna implementacja:

  • Szyfrowanie AES-256 w trybie CBC
  • PBKDF2, aby utworzyć klucz szyfrowania z hasła w postaci zwykłego tekstu
  • HMAC do uwierzytelnienia zaszyfrowanej wiadomości.

Kod i przykłady są tutaj: https://stackoverflow.com/a/19445173/1387163

Eugene Fidelin
źródło
1
Nie jestem ekspertem od kryptografii, ale posiadanie klucza pochodzącego bezpośrednio z hasła wydaje się strasznym pomysłem. Tęczowe stoły + słabe hasło i odszedł to twoje bezpieczeństwo. Również twój link wskazuje na funkcje mcrypt, które są przestarzałe od PHP 7.1
Alph.Dev
@ Alph.Dev masz rację. Powyższa odpowiedź jest ważna tylko dla PHP 5
Eugene Fidelin,