Criptografia bidirecional mais simples usando PHP

230

Qual é a maneira mais simples de fazer criptografia bidirecional em instalações PHP comuns?

Eu preciso ser capaz de criptografar dados com uma chave de cadeia e usar a mesma chave para descriptografar na outra extremidade.

A segurança não é uma preocupação tão grande quanto a portabilidade do código, então eu gostaria de poder manter as coisas o mais simples possível. Atualmente, estou usando uma implementação RC4, mas, se encontrar algo com suporte nativo, acho que posso economizar muito código desnecessário.

user1206970
fonte
3
Para criptografia de uso geral, use defuse / php-encryption / em vez de usar o seu próprio.
Scott Arciszewski
2
Afaste as mãos do github.com/defuse/php-encryption - é mais lento em ordens de magnitude que o mcrypt.
Eugen Rieck
1
@ Scott Pensando na linha de "isso provavelmente não será o gargalo", foi o que nos trouxe muitos softwares ruins.
Eugen Rieck
3
Se você realmente está criptografando / descriptografando muitos dados a ponto de que os milissegundos que custam atrapalhem o seu aplicativo, morda a bala e mude para libsodium. Sodium::crypto_secretbox()e Sodium::crypto_secretbox_open()são seguros e com bom desempenho.
12135 Scott Arciszewski

Respostas:

196

Editado:

Você realmente deveria estar usando openssl_encrypt () & openssl_decrypt ()

Como Scott diz, o Mcrypt não é uma boa ideia, pois não é atualizado desde 2007.

Existe até um RFC para remover o Mcrypt do PHP - https://wiki.php.net/rfc/mcrypt-viking-funeral

472084
fonte
6
@EugenRieck Sim, esse é o ponto. O Mcrypt não recebe patches. O OpenSSL recebe patches assim que qualquer vulnerabilidade é descoberta, grande ou pequena.
Greg
5
seria melhor para uma resposta tão votada, estar lá também fornecendo exemplos mais simples. obrigado de qualquer maneira.
T.Todua
caras, apenas FYI => MCRYPT É DEPRECADO. para que todos saibam não usá-lo, pois isso nos deu uma infinidade de problemas. É obsoleto desde o PHP 7.1, se não me engano.
usar o seguinte comando
Desde o PHP 7, a função mcrypt é removida da base de código php. Portanto, ao usar a versão mais recente do php (que deve ser padrão), você não poderá mais usar esta função obsoleta.
Alexander Behling
234

Importante : A menos que você tenha um caso de uso muito específico, não criptografe senhas , use um algoritmo de hash de senha. Quando alguém diz que criptografa suas senhas em um aplicativo do servidor, elas não são informadas ou estão descrevendo um design de sistema perigoso. Armazenar senhas com segurança é um problema totalmente separado da criptografia.

Ser informado. Projete sistemas seguros.

Criptografia de dados portátil em PHP

Se você estiver usando o PHP 5.4 ou mais recente e não quiser escrever um módulo de criptografia, recomendo usar uma biblioteca existente que forneça criptografia autenticada . A biblioteca que vinculei depende apenas do que o PHP fornece e está sendo revisada periodicamente por um punhado de pesquisadores de segurança. (Eu mesmo incluído.)

Se seus objetivos de portabilidade não impedirem a necessidade de extensões PECL, libsodium é altamente recomendado sobre qualquer coisa que você ou eu possamos escrever em PHP.

Atualização (12/06/2016): agora você pode usar sodium_compat e usar as mesmas ofertas de cripto libsodium sem instalar extensões PECL.

Se você quiser experimentar a engenharia de criptografia, continue lendo.


Primeiro, você deve reservar um tempo para aprender os perigos da criptografia não autenticada e o Princípio da Perdição Criptográfica .

  • Os dados criptografados ainda podem ser adulterados por um usuário mal-intencionado.
  • A autenticação dos dados criptografados impede a violação.
  • A autenticação dos dados não criptografados não impede a violação.

Criptografia e descriptografia

A criptografia no PHP é realmente simples (vamos usar openssl_encrypt()e openssl_decrypt()depois de tomar algumas decisões sobre como criptografar suas informações. Consulte openssl_get_cipher_methods()uma lista dos métodos suportados no seu sistema. A melhor opção é AES no modo CTR :

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

Atualmente, não há razão para acreditar que o tamanho da chave AES seja um problema significativo com o qual se preocupar (maior provavelmente não é melhor, devido ao mau agendamento de teclas no modo de 256 bits).

Nota: Não estamos usando mcryptporque é um abandonware e possui bugs não corrigidos que podem afetar a segurança. Por esses motivos, incentivo outros desenvolvedores de PHP a evitá-lo também.

Wrapper de criptografia / descriptografia simples usando 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;
    }
}

Exemplo de uso

$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);

Demonstração : https://3v4l.org/jl7qR


A biblioteca de criptografia simples acima ainda não é segura de usar. Precisamos autenticar textos cifrados e verificá-los antes de decifrar .

Nota : Por padrão, UnsafeCrypto::encrypt()retornará uma sequência binária bruta. Chame assim se precisar armazená-lo em um formato binário seguro (codificado em 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

Wrapper de autenticação simples

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);
    }
}

Exemplo de uso

$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);

Demonstrações : binário bruto , codificado em base64


Se alguém desejar usar esta SaferCryptobiblioteca em um ambiente de produção ou sua própria implementação dos mesmos conceitos, recomendo que você entre em contato com os criptografadores residentes para obter uma segunda opinião antes de você. Eles poderão falar sobre erros dos quais talvez eu nem saiba.

Você ficará muito melhor usando uma biblioteca de criptografia respeitável .

Scott Arciszewski
fonte
3
Então, estou apenas tentando fazer com que o UnsafeCrypto funcione primeiro. A criptografia acontece bem, mas toda vez que executo a descriptografia, estou ficando 'falso' como resposta. Estou usando a mesma chave para descriptografar e transmitir true na codificação, bem como na decodificação. Há, o que eu assumo é um exemplo no exemplo, eu estou querendo saber se é aí que meu problema está vindo. Você pode explicar de onde vem a variável $ mac e ela deve ser simplesmente $ iv?
David C
1
@EugenRieck As implementações de cifra OpenSSL são provavelmente as únicas partes que não são ruins, e é a única maneira de alavancar o AES-NI no PHP básico. Se você instalar no OpenBSD, o PHP será compilado no LibreSSL sem que o código PHP note uma diferença. Libsodium> OpenSSL a qualquer dia. Além disso, não use libmcrypt . O que você recomendaria que os desenvolvedores do PHP usassem em vez do OpenSSL?
22415 Scott Arciszewski
2
Nem 5.2 nem 5.3 são mais suportados . Em vez disso, você deve procurar atualizar para uma versão suportada do PHP , como a 5.6.
Scott Arciszewski
1
@BBeta paragonie.com/blog/2015/09/…
Scott Arciszewski
1
Eu apenas fiz isso como uma demonstração de que você deseja cordas binárias, não cordas de readabale humano, para suas chaves .
Scott Arciszewski
22

Use mcrypt_encrypt()e mcrypt_decrypt()com os parâmetros correspondentes. Realmente fácil e direto, e você usa um pacote de criptografia testado em batalha.

EDITAR

5 anos e 4 meses após esta resposta, a mcryptextensão está agora em processo de descontinuação e eventual remoção do PHP.

Eugen Rieck
fonte
34
Testado em batalha e não atualizado por mais de 8 anos?
Maarten Bodewes
2
Bem, o mcrypt está no PHP7 e não está obsoleto - isso é bom o suficiente para mim. Nem todo o código é da qualidade horrível do OpenSSL e precisa de correções a cada poucos dias.
Eugen Rieck
3
O mcrypt não é apenas horrível no que diz respeito ao suporte. Também não implementa práticas recomendadas como preenchimento compatível com PKCS # 7, criptografia autenticada. Ele não suporta SHA-3 ou qualquer outro novo algoritmo, pois ninguém o mantém, roubando um caminho de atualização. Além disso, ele costumava aceitar coisas como chaves parciais, realizar preenchimento zero, etc. Há uma boa razão para que ele esteja sendo gradualmente removido do PHP.
Maarten Bodewes
2
No PHP 7.1, todas as funções mcrypt_ * geram um aviso E_DEPRECATED. No PHP 7.1 + 1 (seja 7.2 ou 8.0), a extensão mcrypt será movida para fora do núcleo e para o PECL, onde as pessoas que realmente querem instalá-lo ainda o poderão fazer se puderem instalar extensões PHP a partir do PECL.
Mladen Janjetovic
4

O PHP 7.2 se afastou completamente Mcrypte a criptografia agora é baseada na Libsodiumbiblioteca de manutenção .

Todas as suas necessidades de criptografia podem ser basicamente resolvidas através da Libsodiumbiblioteca.

// 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."
}

Documentação do Libsodium: https://github.com/paragonie/pecl-libsodium-doc

Hemerson Varela
fonte
2
se você colar algum código, verifique se todas as variáveis ​​estão cobertas. No seu exemplo, $ secret_sign_key e $ alice_sign_publickey são NULL
undefinedman
1
A crypto_signAPI não criptografa mensagens - isso exigirá uma das crypto_aead_*_encryptfunções.
Roger Dueck
1

IMPORTANTE, esta resposta é válida apenas para o PHP 5, no PHP 7 use funções criptográficas embutidas.

Aqui está uma implementação simples, mas segura o suficiente:

  • Criptografia AES-256 no modo CBC
  • PBKDF2 para criar chave de criptografia a partir da senha de texto sem formatação
  • HMAC para autenticar a mensagem criptografada.

Código e exemplos estão aqui: https://stackoverflow.com/a/19445173/1387163

Eugene Fidelin
fonte
1
Não sou especialista em criptografia, mas ter uma chave derivada diretamente de uma senha parece uma péssima idéia. Mesas arco-íris + senha fraca e sumiram é sua segurança. Também o seu ponto de ligação para funções mcrypt, que são obsoletas desde o PHP 7.1
Alph.Dev
@ Alph.Dev você está correto, a resposta acima é válida apenas para PHP 5
Eugene Fidelin 10/01