从 PHP 中的 RNCryptor AES 256 标头中检索 IV

Posted

技术标签:

【中文标题】从 PHP 中的 RNCryptor AES 256 标头中检索 IV【英文标题】:Retrieve IV from RNCryptor AES 256 header in PHP 【发布时间】:2012-10-21 18:32:37 【问题描述】:

使用最新的 RNCryptor 源并尝试将加密数据发送到 php 脚本。

RNCryptor 将 IV 打包到标题部分中,该部分附加到实际加密数据中。

- (NSData *)header

  uint8_t header[2] = kRNCryptorFileVersion, self.options;
  NSMutableData *headerData = [NSMutableData dataWithBytes:header length:sizeof(header)];
  if (self.options & kRNCryptorOptionHasPassword) 
    [headerData appendData:self.encryptionSalt]; // 8 bytes
    [headerData appendData:self.HMACSalt]; // 8 bytes
  
  [headerData appendData:self.IV]; // BlockSizeAES128
  return headerData;

我是在 PHP 中处理二进制数据的新手,使用以下解包函数是否正确?

<?
$baseEncodedString = "...";
$data = mb_convert_encoding($baseEncodedString, "UTF-8", "BASE64" );
$array = unpack("Cversion/Coptions/C8salt/C8hmac/C16iv/C*aes", $data);
print_r($array);
?>

注意:加密数据在传输前是从 cocoa 进行 Base64 编码的。

以上PHP脚本返回的数据如...

数组([版本] => 1 [选项] => 1 [salt1] => 109 [salt2] => 195 [盐3] => 185 [盐4] => 71 [盐5] => 130 [盐6] => 209 [盐7] => 230 [salt8] => 25 [hmac1] => 8 [hmac2] => 152 [hmac3] => 188 [hmac4] => 135 [hmac5] => 117 [hmac6] => 169 [hmac7] => 25 [hmac8] => 228 [iv1] => 43 [iv2] => 220 [iv3] => 80 [iv4] => 102 [iv5] => 142 [iv6] => 144 [iv7] => 172 [iv8] => 104 [iv9] => 216 [iv10] => 45 [iv11] => 155 [iv12] => 117 [iv13] => 188 [iv14] => 67 [iv15] => 24 [iv16] => 191 [aes1] => 122 [aes2] => 227 [aes3] => 45 [aes4] => 194 [aes5] => 57 [aes6] => 123 [aes7] => 28 [aes8] => 130 [aes9] => 110 [aes10] => 122 [aes11] => 97 [aes12] => 118 [aes13] => 214 [aes14] => 117 [aes15] => 56 [aes16] => 168 [aes17] => 54 [aes18] => 198 [aes19] => 113 [aes20] => 120 [aes21] => 138 [aes22] => 67 [aes23] => 223 [aes24] => 200 [aes25] => 11 [aes26] => 109 [aes27] => 177 [aes28] => 167 [aes29] => 103 [aes30] => 139 [aes31] => 243 [aes32] => 199 [aes33] => 214 [aes34] => 214 [aes35] => 241 [aes36] => 199 [aes37] => 173 [aes38] => 219 [aes39] => 71 [aes40] => 97 [aes41] => 32 [aes42] => 27 [aes43] => 248 [aes44] => 175 [aes45] => 203 [aes46] => 123 [aes47] => 21)

如何在 PHP MCrypt 函数中使用它?

谢谢。


编辑

为了响应draw010的回答,我已将我的PHP脚本更新为以下...

<?
function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false)

    $algorithm = strtolower($algorithm);
    if(!in_array($algorithm, hash_algos(), true))
        die('PBKDF2 ERROR: Invalid hash algorithm.');
    if($count <= 0 || $key_length <= 0)
        die('PBKDF2 ERROR: Invalid parameters.');

    $hash_length = strlen(hash($algorithm, "", true));
    $block_count = ceil($key_length / $hash_length);

    $output = "";
    for($i = 1; $i <= $block_count; $i++) 
        // $i encoded as 4 bytes, big endian.
        $last = $salt . pack("N", $i);
        // first iteration
        $last = $xorsum = hash_hmac($algorithm, $last, $password, true);
        // perform the other $count - 1 iterations
        for ($j = 1; $j < $count; $j++) 
            $xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true));
        
        $output .= $xorsum;
    

    if($raw_output)
        return substr($output, 0, $key_length);
    else
        return bin2hex(substr($output, 0, $key_length));


$base = $_GET['base'];
$data = mb_convert_encoding($base, "UTF-8", "BASE64" );
//$data = base64_decode($base);

$header = array();
$header['ver'] = substr($data, 0, 1);
$header['options'] = substr($data, 1, 1);
$header['salt'] = substr($data, 2, 8);
$header['hmac'] = substr($data, 10, 8);
$header['iv'] = substr($data, 18, 16);
$data = substr($data, 34);

$td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
mcrypt_generic_init($td, pbkdf2('SHA256', 'password', $header['salt'], 10000, 16), $header['iv']);

//$decrypted = mcrypt_decrypt('rijndael-256','password',$data,'',$header['iv']);
$decrypted = mdecrypt_generic($td, $data);
echo $decrypted;
?>

我怎么还会收到乱码。

U¸¦uÀˆÆ&bŸ8:f`ôShŽºÃ~:¾ÉöÁß=Ç®nqäà€•Æ‹ò

我回顾了 RNCryptor 并为 PHP 脚本使用了以下值

static const RNCryptorSettings kRNCryptorAES256Settings = 
    .algorithm = kCCAlgorithmAES128,
    .blockSize = kCCBlockSizeAES128,
    .IVSize = kCCBlockSizeAES128,
    .options = kCCOptionPKCS7Padding,
    .HMACAlgorithm = kCCHmacAlgSHA256,
    .HMACLength = CC_SHA256_DIGEST_LENGTH,

    .keySettings = 
        .keySize = kCCKeySizeAES256,
        .saltSize = 8,
        .PBKDFAlgorithm = kCCPBKDF2,
        .PRF = kCCPRFHmacAlgSHA1,
        .rounds = 10000
    ,

    .HMACKeySettings = 
        .keySize = kCCKeySizeAES256,
        .saltSize = 8,
        .PBKDFAlgorithm = kCCPBKDF2,
        .PRF = kCCPRFHmacAlgSHA1,
        .rounds = 10000
    
;

我相信这个函数会产生密钥?

+ (NSData *)keyForPassword:(NSString *)password salt:(NSData *)salt settings:(RNCryptorKeyDerivationSettings)keySettings

  NSMutableData *derivedKey = [NSMutableData dataWithLength:keySettings.keySize];

  int result = CCKeyDerivationPBKDF(keySettings.PBKDFAlgorithm,         // algorithm
                                    password.UTF8String,                // password
                                    password.length,                    // passwordLength
                                    salt.bytes,                         // salt
                                    salt.length,                        // saltLen
                                    keySettings.PRF,                    // PRF
                                    keySettings.rounds,                 // rounds
                                    derivedKey.mutableBytes,            // derivedKey
                                    derivedKey.length);                 // derivedKeyLen

  // Do not log password here
  // TODO: Is is safe to assert here? We read salt from a file (but salt.length is internal).
  NSAssert(result == kCCSuccess, @"Unable to create AES key for password: %d", result);

  return derivedKey;

再次感谢。

MCRYPT_RIJNDAEL_128 是否正确?尽管 RNCryptor 设置建议它使用 256,但实际算法是 128,IV 大小与 128 块大小相关。我在某处读过强制 PHP 使用 16 字节 IV,您必须使用 MCRYPT_RIJNDAEL_128 然后让 256 为其提供 32 字节密钥。

【问题讨论】:

【参考方案1】:

这适用于 ios 中最新的 RNCryptor

$b64_data:base64 编码的加密数据$pwd:密码

// back to binary
$bin_data = mb_convert_encoding($b64_data, "UTF-8", "BASE64");
// extract salt
$salt = substr($bin_data, 2, 8);
// extract HMAC salt
$hmac_salt = substr($bin_data, 10, 8);
// extract IV
$iv = substr($bin_data, 18, 16);
// extract data
$data = substr($bin_data, 34, strlen($bin_data) - 34 - 32);
// extract HMAC
$hmac = substr($bin_data, strlen($bin_data) - 32);

// make HMAC key
$hmac_key = $this->pbkdf2('SHA1', $password, $hmac_salt, 10000, 32, true);
// make HMAC hash
$hmac_hash = hash_hmac('sha256', $data , $hmac_key, true);
// check if HMAC hash matches HMAC
if($hmac_hash != $hmac) return false;

// make data key
$key = $this->pbkdf2('SHA1', $password, $salt, 10000, 32, true);
// decrypt
$ret = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $data, MCRYPT_MODE_CBC, $iv);
return trim(preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\xFF]/u', '', $ret));

pbkdf2 与上述问题相同,来自https://defuse.ca/php-pbkdf2.htm。

【讨论】:

请注意,这会剥离 HMAC。如果没有 HMAC,您将无法检测是否有人在途中修改了您的消息。如果攻击者知道部分明文,她可以修改密文,使解密的读取方式不同。例如,如果 Eve 知道 Alice 正在向 Bob 发送 10 美元,她可以将消息更改为向 Eve 发送 99 美元,而无需知道密码。我建议验证 HMAC,因为它可以防止这种攻击。 (即便如此,感谢@bubba_gump 提供的代码。) 我的荣幸,罗布。我有点理解你在说什么,虽然我不够老练,不知道攻击者如何做到这一点。我怎样才能将 HMAC 计算在内以关闭那个洞? 这里是 Python 攻击的分步说明:github.com/rnapier/security-blunders/blob/master/modaes.py 要在 PHP 中验证 HMAC,请使用 hash_hmac。算法是 sha256。数据是上面的$data。如上所述使用 pbkdf2() 生成密钥,只需使用 HMAC salt 而不是加密 salt。将 hash_hmac 的结果与提供的 HMAC 进行比较。如果不匹配,要么密码不正确,要么密文被修改。 更新了我的答案以验证 HMAC。在我的测试中工作。看起来好吗?【参考方案2】:

您不需要为此使用 unpack。

收到完整的 base64 编码字符串后,对其进行解码,现在您应该有一个二进制字符串,该字符串的开头有 IV。

然后您可以使用substr() 从数据中获取您需要的每一部分。

例如:

$base = $_GET['base'];
$data = base64_decode($base);

$iv   = substr($data, 0, 32);  // get 32 byte IV
$data = substr($data, 32);     // set data to begin after the IV now

如果您在密文前附加了其他字段,请确保以正确的顺序对其他数据执行与上述相同的操作。

获得这些数据后,您可以将 $data 连同 IV 和您的密钥一起传递给 mcrypt。

【讨论】:

感谢您的回复,我仍然收到乱码。我已根据您的回答使用修改后的脚本编辑了原件。 只是为了确定,PHP 端的标头字段(salt、IV、版本、hmac 等)是否与 C 端相同?【参考方案3】:

MCRYPT_RIJNDAEL_128 是否正确?尽管 RNCryptor 设置建议它使用 256,但实际算法是 128,IV 大小与 128 块大小相关。我在某处读过强制 PHP 使用 16 字节 IV,您必须使用 MCRYPT_RIJNDAEL_128 然后让 256 为其提供 32 字节密钥。

MCRYPT_RIJNDAEL_128 中的“128”是指块大小,而不是密钥大小。 Rijndael 算法可以处理多种块大小,但 AES 只能处理 128 位块。这与密钥大小无关。 CBC IV 应该始终是块大小,在 AES 中也始终是 16 字节。 (Rijndael 和 AES 非常相似,但并不完全相同。Rijndael 比 AES 更灵活。)

在您的 pbkdf2() 函数中,您应该传递 32 字节(256 位)的密钥长度,而不是 16 字节。我相信如果传递 256 位密钥(基于 Understanding PHP AES Encryption 上的 cmets;我对 mcrypt 不是特别熟悉),PHP mcrypt 模块会自动切换到 256 位 AES。我假设您正在正确实施 PBKDF2;我没有在那里研究过你的代码。

请注意,RNCryptor 在末尾附加了一个 32 字节的 HMAC。我相信您当前的代码会尝试解密它,最终导致 32 字节的垃圾。通常你应该去掉这个 HMAC 并验证它以确保数据在传输过程中没有被修改并且密码是正确的。

【讨论】:

以上是关于从 PHP 中的 RNCryptor AES 256 标头中检索 IV的主要内容,如果未能解决你的问题,请参考以下文章

PHP中的AES加密以在openssl中解密

从 Java 生成的数据中解密 PHP 中的 AES-128

使用 Rijndael_256(AES) 从 PHP mcrypt 解密 Java 中的数据

是否可以在 iOS 上使用 AES128 和 GCM 模式?

PHP 中的 AES-256 加密

PHP中的AES解密