在PHP中重写Rijndael 256 C#加密代码

12
我有一个用C#编写的加密/解密算法 - 我需要能够在PHP中生成相同的加密文本,以便我可以通过HTTP发送加密文本,在C#端进行解密。 以下是加密的C#代码。
this.m_plainText = string.Empty;
this.m_passPhrase = "passpharse";
this.m_saltValue = "saltvalue";
this.m_hashAlgorithm = "SHA1";
this.m_passwordIterations = 2;
this.m_initVector = "1a2b3c4d5e6f7g8h";
this.m_keySize = 256;

public string Encrypt()
{
    string plainText = this.m_plainText;
    string passPhrase = this.m_passPhrase;
    string saltValue = this.m_saltValue;
    string hashAlgorithm = this.m_hashAlgorithm;
    int passwordIterations = this.m_passwordIterations;
    string initVector = this.m_initVector;
    int keySize = this.m_keySize;

    // Convert strings into byte arrays.
    // Let us assume that strings only contain ASCII codes.
    // If strings include Unicode characters, use Unicode, UTF7, or UTF8 
    // encoding.
    byte[] initVectorBytes = Encoding.ASCII.GetBytes(initVector);
    byte[] saltValueBytes = Encoding.ASCII.GetBytes(saltValue);

    // Convert our plaintext into a byte array.
    // Let us assume that plaintext contains UTF8-encoded characters.
    byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);

    // First, we must create a password, from which the key will be derived.
    // This password will be generated from the specified passphrase and 
    // salt value. The password will be created using the specified hash 
    // algorithm. Password creation can be done in several iterations.
    PasswordDeriveBytes password = new PasswordDeriveBytes(
                                                    passPhrase,
                                                    saltValueBytes,
                                                    hashAlgorithm,
                                                    passwordIterations);

    // Use the password to generate pseudo-random bytes for the encryption
    // key. Specify the size of the key in bytes (instead of bits).
    byte[] keyBytes = password.GetBytes(keySize / 8);

    // Create uninitialized Rijndael encryption object.
    RijndaelManaged symmetricKey = new RijndaelManaged();

    // It is reasonable to set encryption mode to Cipher Block Chaining
    // (CBC). Use default options for other symmetric key parameters.
    symmetricKey.Mode = CipherMode.CBC;

    // Generate encryptor from the existing key bytes and initialization 
    // vector. Key size will be defined based on the number of the key 
    // bytes.
    ICryptoTransform encryptor = symmetricKey.CreateEncryptor(
                                                     keyBytes,
                                                     initVectorBytes);

    // Define memory stream which will be used to hold encrypted data.
    MemoryStream memoryStream = new MemoryStream();

    // Define cryptographic stream (always use Write mode for encryption).
    CryptoStream cryptoStream = new CryptoStream(memoryStream,
                                                 encryptor,
                                                 CryptoStreamMode.Write);
    // Start encrypting.
    cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);

    // Finish encrypting.
    cryptoStream.FlushFinalBlock();

    // Convert our encrypted data from a memory stream into a byte array.
    byte[] cipherTextBytes = memoryStream.ToArray();

    // Close both streams.
    memoryStream.Close();
    cryptoStream.Close();

    // Convert encrypted data into a base64-encoded string.
    string cipherText = Convert.ToBase64String(cipherTextBytes);

    // Return encrypted string.
    return cipherText;
}

我有一些类似的 PHP 代码可能会有所帮助。它不能完全满足需要,但我认为这可能是一个很好的起点。
<?php

/*
 * DEFINE CONSTANTS
 */
$HashPassPhrase = "passpharse";
$HashSalt = "saltvalue";
$HashAlgorithm = "SHA1";
$HashIterations = "2";
$InitVector = "1a2b3c4d5e6f7g8h";        // Must be 16 bytes
$keySize = "256";

class Cipher {
    private $securekey, $iv;
    function __construct($textkey) {
        $this->securekey = hash($HashAlgorithm,$textkey,TRUE);
        $this->iv = $InitVector;
    }
    function encrypt($input) {
        return base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $this->securekey, $input, MCRYPT_MODE_CBC, $this->iv));
    }
    function decrypt($input) {
        return trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $this->securekey, base64_decode($input), MCRYPT_MODE_CBC, $this->iv));
    }
}

$cipher = new Cipher($HashPassPhrase);

$encryptedtext = $cipher->encrypt("Text To Encrypt");
echo "->encrypt = $encryptedtext<br />";

$decryptedtext = $cipher->decrypt($encryptedtext);
echo "->decrypt = $decryptedtext<br />";

var_dump($cipher);

?>

4个回答

17
您需要以与C#代码中的PasswordDeriveBytes相同的方式从密码短语中派生密钥。根据RFC2898的规定,这被记录为执行PBKDF1密钥派生:

该类使用PKCS#5 v2.0标准中定义的PBKDF1算法的扩展来从密码中派生适用于用作密钥材料的字节。该标准在IETF RRC 2898中记录。

有PHP库实现PBKDF1,但是基于RFC从头编写一个非常简单:

PBKDF1 (P, S, c, dkLen)

Options: Hash
underlying hash function

Input: P
password, an octet string S salt, an eight-octet string c iteration count, a positive integer dkLen intended length in octets of derived key, a positive integer, at most 16 for MD2 or MD5 and 20 for SHA-1

Output: DK derived key, a dkLen-octet string

Steps:

  1. If dkLen > 16 for MD2 and MD5, or dkLen > 20 for SHA-1, output
     "derived key too long" and stop.

  2. Apply the underlying hash function Hash for c iterations to the
     concatenation of the password P and the salt S, then extract
     the first dkLen octets to produce a derived key DK:

               T_1 = Hash (P || S) ,
               T_2 = Hash (T_1) ,
               ...
               T_c = Hash (T_{c-1}) ,
               DK = Tc<0..dkLen-1>

  3. Output the derived key DK.

更新

当您发现自己处于这种情况时,通常会搜索一个示例实现,以显示每个步骤的值。例如http://www.di-mgt.com.au/cryptoKDFs.html#examplespbkdf上的示例实现:

Password = "password" 
         = (0x)70617373776F7264
Salt     = (0x)78578E5A5D63CB06
Count    = 1000
kLen     = 16
Key      = PBKDF1(Password, Salt, Count, kLen)
         = (0x)DC19847E05C64D2FAF10EBFB4A3D2A20

P || S = 70617373776F726478578E5A5D63CB06
T_1=     D1F94C4D447039B034494400F2E7DF9DCB67C308
T_2=     2BB479C1D369EA74BB976BBA2629744E8259C6F5
...
T_999=   6663F4611D61571068B5DA168974C6FF2C9775AC
T_1000=  DC19847E05C64D2FAF10EBFB4A3D2A20B4E35EFE
Key=     DC19847E05C64D2FAF10EBFB4A3D2A20

现在让我们编写一个PHP函数来实现这个功能:
function PBKDF1($pass,$salt,$count,$dklen) { 
    $t = $pass.$salt;
    //echo 'S||P: '.bin2hex($t).'<br/>';
    $t = sha1($t, true); 
    //echo 'T1:' . bin2hex($t) . '<br/>';
    for($i=2; $i <= $count; $i++) { 
        $t = sha1($t, true); 
        //echo 'T'.$i.':' . bin2hex($t) . '<br/>';
    } 
    $t = substr($t,0,$dklen); 
    return $t;      
}

现在你可以看到你的错误:你没有指定非常重要的raw=true参数给sha1。让我们来看看我们函数的输出:
$HashPassPhrase = pack("H*","70617373776F7264");
$HashSalt = pack("H*","78578E5A5D63CB06"); 
$HashIterations = 1000; 
$devkeylength = 16; 
$devkey = PBKDF1($HashPassPhrase,$HashSalt,$HashIterations,$devkeylength);
echo 'Key:' . bin2hex(substr($devkey, 0, 8)) . '<br/>';
echo 'IV:' . bin2hex(substr($devkey, 8, 8)) .'<br/>';
echo 'Expected: DC19847E05C64D2FAF10EBFB4A3D2A20<br/>';

这个输出正是所期望的结果:
Key:dc19847e05c64d2f
IV:af10ebfb4a3d2a20
Expected: DC19847E05C64D2FAF10EBFB4A3D2A20

接下来,我们可以验证C#函数是否相同:
            byte[] password = Encoding.ASCII.GetBytes("password");
            byte[] salt = new byte[] { 0x78, 0x57, 0x8e, 0x5a, 0x5d, 0x63, 0xcb, 0x06};

            PasswordDeriveBytes pdb = new PasswordDeriveBytes(
                password, salt, "SHA1", 1000);

            byte[] key = pdb.GetBytes(8);
            byte[] iv = pdb.GetBytes(8);

            Console.Out.Write("Key: ");
            foreach (byte b in key)
            {
                Console.Out.Write("{0:x} ", b);
            }
            Console.Out.WriteLine();

            Console.Out.Write("IV: ");
            foreach (byte b in iv)
            {
                Console.Out.Write("{0:x} ", b);
            }
            Console.Out.WriteLine();

这将生成完全相同的输出:
Key: dc 19 84 7e 5 c6 4d 2f
IV: af 10 eb fb 4a 3d 2a 20

QED

额外解释

如果您不确切知道自己在做什么,请不要进行加密操作。即使您已经正确实现了PHP,您发布的C#代码也存在一些严重问题。您将字节数组与表示十六进制转储的字符串混合使用,您使用硬编码IV而非从口令和盐派生出来的IV,这样就是完全错误的。请使用现成的加密方案,如SSL或S-MIME,不要重新发明轮子,否则您将会出错。


// warning: no checking on $dklen, must be 0..20 function PBKDF1($pass,$salt,$count,$dklen) { $t = $pass.$salt;; for($i=0; $i < $count; $i++) { $t = sha1($t, true); } return substr($t,0,$dklen); } - Maarten Bodewes
@owlstead:你说得对,实际上我的示例显示PHP输出以4a3d2a结尾,而C#示例以4a 3d 2a 20结尾...已经修复了。我想我被RFC中的dklen-1冲昏了头脑。 - Remus Rusanu
如果我每犯一次“差一”的错误就能得到五分镍币,那该多好啊... :) - Maarten Bodewes
1
重要提示:PasswordDeriveBytes仅与PBKDF1兼容,当且仅当请求的长度为20个字节或更少时,这是使用SHA1的PBKDF1的最大输出。 PasswordDeriveBytes使用专有方案,不安全甚至不一致(例如,请求32字节然后16字节会创建与请求48字节不同的结果)。有关更多信息,请参见此处。 - Maarten Bodewes
1
重要提示#2:它甚至会“重复字节”,这意味着你的IV正在泄露关键信息。 - Maarten Bodewes
显示剩余4条评论

4
看起来您的主要问题是在C#端使用了PHP的hash()而不是PasswordDeriveBytes()步骤。这两种方法并不等价。后者实现了PBKDF1密码派生算法,而hash()只是一个哈希函数。看起来PEAR可能有一个PBKDF1实现,否则您可能需要自己编写。
同时,如果您还没有这样做的话,您需要确保双方的文本编码一致。
最后,考虑不要做您正在做的事情,因为加密比它看起来更难。由于您正在使用HTTP,您可以利用SSL协议,而不是编写自己的协议。这将为您带来更好的安全性和减少在低级细节(如保持增量IV同步等)上的麻烦。

谢谢您的回复。是的,我意识到主要问题在于PasswordDeriveBytes()。您的意思是,通过HTTPS和SSL传递明文密码比在一侧进行编码并在另一侧进行解码更安全吗? - Derek Armstrong
@Derek Armstrong:是的!SSL旨在保护像明文密码这样的东西,就像你的方案一样。主要区别在于,SSL已经由地球上最好的密码学家进行了15年的审查和修订。我认为它也会更容易使用,特别是因为您已经在使用HTTP通信。 - ladenedge
1
我也在Remus的回答中添加了一条注释,PasswordDeriveBytes是一个不一致、未记录的专有方案,对于从它请求的所有字节超过PBKDF1的最大输出(20字节),它都是如此。 - Maarten Bodewes

0

那个模块比他已经拥有的那个模块还要更低级! - ladenedge
此函数已于PHP 7.1.0版本中被弃用,并在PHP 7.2.0版本中被移除。 - Ahmad

0

检查PHP中的OpenSSL例程,它们应该能够处理您需要执行的操作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接