Mã hóa hai chiều đơn giản nhất bằng PHP


230

Cách đơn giản nhất để thực hiện mã hóa hai chiều trong các bản cài đặt PHP thông thường là gì?

Tôi cần có khả năng mã hóa dữ liệu bằng khóa chuỗi và sử dụng cùng khóa để giải mã ở đầu bên kia.

Bảo mật không phải là mối quan tâm lớn như tính di động của mã, vì vậy tôi muốn có thể giữ mọi thứ đơn giản nhất có thể. Hiện tại, tôi đang sử dụng triển khai RC4, nhưng nếu tôi có thể tìm thấy thứ gì đó được hỗ trợ nguyên bản, tôi có thể tiết kiệm được rất nhiều mã không cần thiết.



3
Để mã hóa cho mục đích chung, hãy sử dụng defuse / php-mã hóa / thay vì tự cuộn.
Scott Arciszewski

2
Tránh xa github.com/defuse/php-encoding - tốc độ chậm hơn so với mcrypt.
Eugen Rieck

1
@Scott Suy nghĩ theo dòng "điều này có thể sẽ không phải là nút cổ chai" là điều mang lại cho chúng tôi rất nhiều phần mềm xấu.
Eugen Rieck

3
Nếu bạn thực sự mã hóa / giải mã rất nhiều dữ liệu đến mức một phần nghìn giây, nó sẽ làm mất ứng dụng của bạn, hãy cắn viên đạn và chuyển sang libsodium. Sodium::crypto_secretbox()Sodium::crypto_secretbox_open()được an toàn và thực hiện.
Scott Arciszewski

Câu trả lời:


196

Đã chỉnh sửa:

Bạn thực sự nên sử dụng openssl_encrypt () & openssl_decrypt ()

Như Scott nói, Mcrypt không phải là một ý tưởng hay vì nó chưa được cập nhật kể từ năm 2007.

Thậm chí còn có một RFC để loại bỏ Mcrypt khỏi PHP - https://wiki.php.net/rfc/mcrypt-viking-funeral


6
@EugenRieck Vâng, đó là điểm chính. Mcrypt không nhận được bản vá. OpenSSL nhận các bản vá ngay khi phát hiện bất kỳ lỗ hổng nào, dù lớn hay nhỏ.
Greg

5
sẽ tốt hơn cho câu trả lời được bình chọn cao như vậy, để được cung cấp các ví dụ đơn giản nhất trong câu trả lời. Dẫu sao cũng xin cảm ơn.
T.Todua

các bạn, chỉ cần FYI => MCRYPT ĐƯỢC ĐỔI. mũ để mọi người nên biết không sử dụng nó vì nó đã cho chúng ta vô số vấn đề. Nó không dùng nữa từ PHP 7.1 nếu tôi không nhầm.
clusterBuddy

Vì PHP 7, hàm mcrypt bị xóa khỏi php codebase. Vì vậy, khi sử dụng phiên bản mới nhất của php (phải là tiêu chuẩn), bạn không thể sử dụng chức năng không dùng nữa này.
Alexander Behling

234

Quan trọng : Trừ khi bạn có trường hợp sử dụng rất đặc biệt, không mã hóa mật khẩu , thay vào đó hãy sử dụng thuật toán băm mật khẩu. Khi ai đó nói rằng họ mã hóa mật khẩu của họ trong một ứng dụng phía máy chủ, họ sẽ không hiểu rõ hoặc họ đang mô tả một thiết kế hệ thống nguy hiểm. Lưu trữ an toàn mật khẩu là một vấn đề hoàn toàn tách biệt với mã hóa.

Được thông báo. Thiết kế hệ thống an toàn.

Mã hóa dữ liệu di động trong PHP

Nếu bạn đang sử dụng PHP 5.4 trở lên và không muốn tự mình viết mô-đun mật mã, tôi khuyên bạn nên sử dụng thư viện hiện có cung cấp mã hóa được xác thực . Thư viện mà tôi liên kết chỉ dựa trên những gì PHP cung cấp và đang được một số nhà nghiên cứu bảo mật xem xét định kỳ. (Bao gồm cả bản thân tôi.)

Nếu các mục tiêu về tính di động của bạn không ngăn được yêu cầu các phần mở rộng PECL, libsodium được khuyến nghị cao hơn bất cứ thứ gì bạn hoặc tôi có thể viết trong PHP.

Cập nhật (2016-06-12): Bây giờ bạn có thể sử dụng sodium_compat và sử dụng cùng các ưu đãi libsodium của tiền điện tử mà không cần cài đặt các tiện ích mở rộng PECL.

Nếu bạn muốn thử sức mình với kỹ thuật mật mã, hãy đọc tiếp.


Đầu tiên, bạn nên dành thời gian để tìm hiểu sự nguy hiểm của mã hóa không được xác thựcNguyên tắc Doom Mật mã .

  • Dữ liệu được mã hóa vẫn có thể bị giả mạo bởi người dùng độc hại.
  • Xác thực dữ liệu được mã hóa ngăn chặn giả mạo.
  • Xác thực dữ liệu không được mã hóa không ngăn chặn giả mạo.

Mã hóa và giải mã

Mã hóa trong PHP thực sự đơn giản (chúng tôi sẽ sử dụng openssl_encrypt()openssl_decrypt()một khi bạn đã đưa ra một số quyết định về cách mã hóa thông tin của mình. Tham khảo openssl_get_cipher_methods()danh sách các phương thức được hỗ trợ trên hệ thống của bạn. Lựa chọn tốt nhất là AES trong chế độ CTR :

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

Hiện tại không có lý do nào để tin rằng kích thước khóa AES là vấn đề đáng lo ngại (lớn hơn có lẽ không tốt hơn, do lập lịch khóa không tốt trong chế độ 256 bit).

Lưu ý: Chúng tôi không sử dụng mcryptvì đây là phần mềm bị bỏ rơi và có các lỗi chưa được vá có thể ảnh hưởng đến bảo mật. Vì những lý do này, tôi khuyến khích các nhà phát triển PHP khác cũng tránh nó.

Gói mã hóa / giải mã đơn giản sử dụng 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;
    }
}

Ví dụ sử dụng

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

Bản trình diễn : https://3v4l.org/jl7qR


Thư viện tiền điện tử đơn giản ở trên vẫn không an toàn để sử dụng. Chúng ta cần xác thực các bản mã và xác minh chúng trước khi giải mã .

Lưu ý : Theo mặc định, UnsafeCrypto::encrypt()sẽ trả về một chuỗi nhị phân thô. Gọi nó như thế này nếu bạn cần lưu trữ nó ở định dạng an toàn nhị phân (mã hóa 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);

Bản trình diễn : http://3v4l.org/f5K93

Gói xác thực đơn giản

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

Ví dụ sử dụng

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

Trình diễn : nhị phân thô , mã hóa base64


Nếu bất cứ ai muốn sử dụng SaferCryptothư viện này trong môi trường sản xuất hoặc thực hiện các khái niệm tương tự của riêng bạn, tôi khuyên bạn nên liên hệ với các nhà mật mã thường trú để có ý kiến ​​thứ hai trước khi bạn thực hiện. Họ sẽ có thể nói với bạn về những sai lầm mà tôi thậm chí không nhận thức được.

Bạn sẽ tốt hơn nhiều khi sử dụng một thư viện mật mã có uy tín .


3
Vì vậy, tôi chỉ đang cố gắng để UnsafeCrypto hoạt động trước. Việc mã hóa diễn ra tốt đẹp, nhưng mỗi khi tôi chạy giải mã, tôi lại nhận được "sai" là phản hồi. Tôi đang sử dụng cùng một khóa để giải mã và chuyển đúng sang mã hóa, cũng như giải mã. Có, những gì tôi giả sử là một loại trong ví dụ, tôi tự hỏi nếu đó là vấn đề của tôi đến từ đâu. Bạn có thể giải thích biến $ mac đến từ đâu không, và nó có nên đơn giản là $ iv không?
David C

1
@EugenRieck Việc triển khai mật mã OpenSSL có lẽ là phần duy nhất không hút và đó là cách duy nhất để tận dụng AES-NI trong vanilla PHP. Nếu bạn cài đặt trên OpenBSD, PHP sẽ được biên dịch dựa trên LibreSSL mà không có mã PHP nhận thấy sự khác biệt. Libsodium> OpenSSL bất cứ ngày nào. Ngoài ra, không sử dụng libmcrypt . Bạn muốn giới thiệu những gì các nhà phát triển PHP sử dụng thay vì OpenSSL?
Scott Arciszewski

2
Cả 5.2 và 5.3 đều không được hỗ trợ nữa . Thay vào đó, bạn nên xem xét cập nhật lên phiên bản PHP được hỗ trợ , chẳng hạn như 5.6.
Scott Arciszewski


1
Tôi chỉ làm điều đó như một minh chứng cho việc bạn muốn các chuỗi nhị phân, chứ không phải các chuỗi đọc của con người, cho các khóa của bạn .
Scott Arciszewski

22

Sử dụng mcrypt_encrypt()mcrypt_decrypt()với các thông số tương ứng. Thực sự dễ dàng và thẳng tiến, và bạn sử dụng gói mã hóa được thử nghiệm trong trận chiến.

BIÊN TẬP

5 năm và 4 tháng sau câu trả lời này, mcryptphần mở rộng hiện đang trong quá trình phản đối và loại bỏ cuối cùng khỏi PHP.


34
Trận thử nghiệm và không được cập nhật trong hơn 8 năm?
Maarten Bodewes

2
Chà, mcrypt có trong PHP7 và không bị phản đối - điều đó đủ tốt cho tôi. Không phải tất cả các mã đều có chất lượng khủng khiếp của OpenSSL và cần vá mỗi vài ngày.
Eugen Rieck

3
mcrypt không chỉ là khủng khiếp đối với hỗ trợ. Nó cũng không thực hiện các thực tiễn tốt nhất như phần đệm tuân thủ PKCS # 7, mã hóa được xác thực. Nó sẽ không hỗ trợ SHA-3 hoặc bất kỳ thuật toán mới nào khác vì không ai duy trì nó, cướp đi con đường nâng cấp của bạn. Hơn nữa, nó được sử dụng để chấp nhận những thứ như khóa một phần, thực hiện đệm không, v.v ... Có một lý do chính đáng tại sao nó đang trong quá trình dần dần bị xóa khỏi PHP.
Maarten Bodewes

2
Trong PHP 7.1, tất cả các hàm mcrypt_ * sẽ đưa ra thông báo E_DEPRECATED. Trong PHP 7.1 + 1 (có thể là 7.2 hoặc 8.0), phần mở rộng mcrypt sẽ được chuyển ra khỏi lõi và vào PECL, nơi những người thực sự muốn cài đặt nó vẫn có thể làm như vậy nếu họ có thể cài đặt các phần mở rộng PHP từ PECL.
Mladen Janjetovic

4

PHP 7.2 đã chuyển hoàn toàn khỏi Mcryptvà mã hóa bây giờ dựa trên Libsodiumthư viện có thể bảo trì .

Tất cả các nhu cầu mã hóa của bạn về cơ bản có thể được giải quyết thông qua Libsodiumthư viện.

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

Tài liệu về Libsodium: https://github.com/paragonie/pecl-libsodium-doc


2
nếu bạn dán một số mã, đảm bảo rằng tất cả các biến được bảo hiểm. Trong ví dụ của bạn $ secret_sign_key và $ alice_sign_publickey là NULL
không xác định

1
Các crypto_signAPI thực hiện không thông điệp mã hóa - đó sẽ yêu cầu một trong những crypto_aead_*_encryptchức năng.
Roger Dueck

1

QUAN TRỌNG câu trả lời này chỉ hợp lệ cho PHP 5, trong PHP 7 sử dụng các hàm mã hóa tích hợp.

Đây là cách thực hiện đơn giản nhưng đủ an toàn:

  • Mã hóa AES-256 ở chế độ CBC
  • PBKDF2 để tạo khóa mã hóa bằng mật khẩu văn bản đơn giản
  • HMAC để xác thực tin nhắn được mã hóa.

Mã và ví dụ có tại đây: https://stackoverflow.com/a/19445173/1387163


1
Tôi không phải là một chuyên gia về mật mã, nhưng có một khóa được lấy trực tiếp từ mật khẩu có vẻ như là ý tưởng khủng khiếp. Bảng cầu vồng + mật khẩu yếu và mất đi là bảo mật của bạn. Đồng thời, liên kết của bạn trỏ đến các hàm mcrypt, không được dùng nữa kể từ PHP 7.1
Alph.Dev

@ Alph.Dev bạn đúng câu trả lời ở trên chỉ có giá trị cho PHP 5
Eugene Fidelin
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.