【问题标题】:PHP implementing Ciphertext Stealing (CTS) with CBCPHP 使用 CBC 实现密文窃取 (CTS)
【发布时间】:2021-11-20 14:51:00
【问题描述】:

我一直在尝试在 PHP 中为 CBC 实现密文窃取(CTS)。

参考以下两个链接

How can I encrypt/decrypt data using AES CBC+CTS (ciphertext stealing) mode in PHP?

http://en.wikipedia.org/wiki/Ciphertext_stealing

我很困惑并停留在 XOR 的最后也是最简单的一步。 我知道这很愚蠢,但尝试了所有组合后,我不知道我错过了什么。 代码如下。

// 1. Decrypt the second to last ciphertext block, using zeros as IV.       
$second_to_last_cipher_block = substr($cipher_text, strlen($cipher_text) - 32, 16);     
$second_to_last_plain = @mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $second_to_last_cipher_block, MCRYPT_MODE_CBC);

// 2. Pad the ciphertext to the nearest multiple of the block size using the last B-M 
//    bits of block cipher decryption of the second-to-last ciphertext block.
$n = 16 - (strlen($cipher_text) % 16);
$cipher_text .= substr($second_to_last_plain, -$n);

// 3. Swap the last two ciphertext blocks.
$cipher_block_last = substr($cipher_text, -16);
$cipher_block_second_last = substr($cipher_text, -32, 16);
$cipher_text = substr($cipher_text, 0, -32) . $cipher_block_last . $cipher_block_second_last;

// 4. Decrypt the ciphertext using the standard CBC mode up to the last block.
$cipher = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
mcrypt_generic_init($cipher, $key, $iv);
$plain_text = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $cipher_text, MCRYPT_MODE_CBC , $iv);

// 5. Exclusive-OR the last ciphertext (was already decrypted in step 1) with the second last ciphertext.
//    ???
//    echo $??? ^ $???;

【问题讨论】:

    标签: php cryptography aes lockbox-3 cbc-mode


    【解决方案1】:

    我发现具体的用例对理解算法非常有帮助。以下是 2 个用例和分步演练。

    两个用例的起点。

    这些用例假设您使用 AES-256 和 CBC 链接模式和密文窃取来解密消息以进行块量化。为了生成这些用例,我使用了 Delphi 2010 编译器和 TurboPower LockBox3 库(SVN 修订版 243)。在下文中,我使用这样的符号......

    IV := [16] 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    

    ... 表示某个名为“IV”的变量被分配为等于 16 个字节的数组。最左边的字节是数组的 Least Signficant(最低地址)字节的呈现,最右边的字节是最重要的字节。这些字节是用十六进制写的,例如,如果一个放...

    X := [2] 03 10
    

    ...表示LSB为3,MSB为16。

    用例一

    1. 让 AES-256 32 字节压缩密钥(在 AES 标准中定义)为...

      key = [32] 0D EE 8F 9F 8B 0B D4 A1 17 59 FA 05 FA 2B 65 4F 23 00 29 26 0D EE 8F 9F 8B 0B D4 A1 17 59 FA 05
      

      使用 TurboPower LockBox 3,可以通过将 TCodec 组件的密码 ('UTF8Password') 属性设置为...

      password = (UTF-8) 'Your lips are smoother than vasoline.'
      
    2. 要发送的明文消息将是

      Message = (UTF-8) 'Leeeeeeeeeroy Jenkins!'
      

      编码为 22 字节长。 AES-256 的块大小为 16 字节,因此长度介于 1 到 2 个块之间。

    3. 让IV为1。(顺便说一句:在Delphi方面,这可以通过设置来实现

      TRandomStream.Instance.Seed := 1;
      

      就在加密之前)。 因此,要由 PHP 解密的密文消息将是(带有 8 字节 IV 前面的 la LockBox3)...

      ciphertext = [30] 01 00 00 00 00 00 00 00 17 5C C0 97 FF EF 63 5A 88 83 6C 00 62 BF 87 E5 1D 66 DB 97 2E 2C
      (base64 equivalent ='AQAAAAAAAAAXXMCX/+9jWoiDbABiv4flHWbbly4s')
      

      将其分解为 IV,第一个密文块 (c[0]) 和最后一个(部分)密文块 (c[1])...

      IV = [16] 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
      c[0] = [16] 17 5C C0 97 FF EF 63 5A 88 83 6C 00 62 BF 87 E5
      c[1] = [6] 1D 66 DB 97 2E 2C
      
    4. 现在让我们通过密文窃取来演练解密。

      • CV := IV

        CV = [16] 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
        
      • 一般来说,对于第n个块(除了最后2个块),我们正常的CBC算法是……

        m[n]    := Decrypt( c[n]) XOR CV;
        CV[n+1] := c[n]
        

        地点:

        • m为输出明文块;
        • Decrypt() 表示对该块进行 AES-256 ECB 解密;
        • CV 是我们的进位向量。链接模式定义了这如何从一个块到另一个块的变化。
      • 但对于倒数第二个块 (N-1)(在用例一中 N=2),转换变为 ...(由于选择了密文窃取而发生此异常 )

        m[n]    := Decrypt( c[n]) XOR CV;
        CV[n+1] := CV[n] // Unchanged!
        
      • 应用于我们的用例:

        CV = [16] 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
        c[0] = [16] 17 5C C0 97 FF EF 63 5A 88 83 6C 00 62 BF 87 E5
        Decrypt(c[0]) = [16] 6F 6B 69 6E 73 21 F0 7B 79 F2 AF 27 B1 52 D6 0B
        m[0] := Decrypt(c[0]) XOR CV = [16] 6E 6B 69 6E 73 21 F0 7B 79 F2 AF 27 B1 52 D6 0B
        
    5. 现在处理最后一个块。它是一个部分的,长 6 个字节。一般来说,最后一个block的处理是这样的……

      y := c[N-1] | LastBytes( m[N-2], BlockSize-Length(c[N-1]));
      m[N-1] := Decrypt( y) XOR CV 
      

      应用于用例一:

      c[1] = [6] 1D 66 DB 97 2E 2C
      y := c[1] | LastBytes( m[0], 10)
      y = [16] 1D 66 DB 97 2E 2C F0 7B 79 F2 AF 27 B1 52 D6 0B
      Decrypt( y) = [16]= 4D 65 65 65 65 65 65 65 65 65 72 6F 79 20 4A 65
      m[1] := Decrypt(y) XOR CV
      m[1] = [16] 4C 65 65 65 65 65 65 65 65 65 72 6F 79 20 4A 65
      
    6. 解密过程的最后一步是最后两个块的发射。我们颠倒顺序,先发出 m[N-1],然后发出 m[N-2] 的第一部分(其长度等于 c[N-1] 的长度)。申请用例一...

      • 发射 m[1]

        m[1] = [16] 4C 65 65 65 65 65 65 65 65 65 72 6F 79 20 4A 65
        
      • 发出 m[0] 的前 6 个字节

        FirstBytes( m[0], 6) = 6E 6B 69 6E 73 21
        
      • 总而言之,我们得到了...的重构明文

        [22] 4C 65 65 65 65 65 65 65 65 65 72 6F 79 20 4A 65 6E 6B 69 6E 73 21
        

      这是“Leeeeeeeeeeroy Jenkins!”的 UTF-8 编码!

    用例二

    在这个用例中,消息正好是 2 个块长。这称为圆形外壳。在圆形情况下,没有要量化的部分块,因此它就像正常的 CBC 一样进行。密码、密钥和 IV 与用例一中的相同。要解密的密文消息(包括前置8字节IV)是...

    1. 设置

      Ciphertext = [40] 01 00 00 00 00 00 00 00 70 76 12 58 4E 38 1C E1 92 CA 34 FB 9A 37 C5 0A 75 F2 0B 46 A1 DF 56 60 D4 5C 76 4B 52 19 DA 83
      which is encoded base64 as 'AQAAAAAAAABwdhJYTjgc4ZLKNPuaN8UKdfILRqHfVmDUXHZLUhnagw=='
      

      这分解为IV,第一块和第二块,就像这样......

      IV = [16] 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
      c[0] = [16] 70 76 12 58 4E 38 1C E1 92 CA 34 FB 9A 37 C5 0A
      c[1] = [16] 75 F2 0B 46 A1 DF 56 60 D4 5C 76 4B 52 19 DA 83
      
    2. 一般和倒数第二块

      Decrypt(c[0]) = [16] 45 61 6E 63 65 20 74 68 65 6E 2C 20 77 68 65 72
      m[0] := Decrypt(c[0]) XOR CV = [16] 44 61 6E 63 65 20 74 68 65 6E 2C 20 77 68 65 72
      Next CV := c[0] = [16] 70 76 12 58 4E 38 1C E1 92 CA 34 FB 9A 37 C5 0A
      
    3. 最后一块:

      在这个用例中,我们的最后一个块是圆形的。

      Decrypt(c[1]) = [16] 75 F2 0B 46 A1 DF 56 60 D4 5C 76 4B 52 19 DA 83
      m[1] := Decrypt(c[1]) XOR CV = [16] 65 65 76 65 72 20 79 6F 75 20 6D 61 79 20 62 65
      
    4. 解密过程的最后一步是最后两个块的发射。在圆形情况下,我们不会颠倒顺序。我们先发出 m[N-2],然后发出 m[N-1]。申请用例二...

      • 发射 m[0]

        m[0] = [16] 44 61 6E 63 65 20 74 68 65 6E 2C 20 77 68 65 72
        
      • 发射整个m1

        m[1] = [16] 65 65 76 65 72 20 79 6F 75 20 6D 61 79 20 62 65
        
      • 总而言之,我们得到了...的重构明文

        [32] 44 61 6E 63 65 20 74 68 65 6E 2C 20 77 68 65 72 65 65 76 65 72 20 79 6F 75 20 6D 61 79 20 62 65
        

      这是 UTF-8 编码的“然后,无论你身在何处”

    5. 要考虑的边缘情况。 有两种极端情况,此处提供的两个用例未说明。

      • 短消息。短消息是一条消息,其长度以字节为单位:

        • 不为零;和
        • 不到一格;
      • 零长度消息。

    在短消息的情况下,从技术上讲,仍然可以通过使用 IV 作为密文的先验块来实现密文窃取。然而,恕我直言,这种使用密文窃取的方式,由于缺乏对密码强度影响的研究,更不用说增加的实现复杂性,是不合理的。在 TurboPower LockBox 3 中,当消息是短消息,并且链模式不是密钥流时,链模式被视为 CFB-8bit。 CFB-8 位是一种密钥流模式。

    在零长度消息的情况下,它真的很简单。零长度明文消息一对一映射到零长度密文消息。不需要、生成或预先添加 IV。此映射独立于链接模式和密码(在块模式密码的情况下)。

    PHP 实现注意事项

    警告

    我不是 PHP 程序员。我不知道PHP。我在这里说的任何话都应该持保留态度。

    字节数组

    看起来您正在使用 PHP 字符串来存储字节数组。这对我来说看起来很危险。如果其中一个字节值为零怎么办?那会缩短字符串吗? strlen() 在这种情况下会如何表现?如果 PHP 有一个本地数据类型,它是一个字节数组,那么这可能会更安全。但我真的不知道。我只是提请您注意这一点,如果您还没有意识到的话。可能这不是一个真正的问题。

    mcrypt_decrypt 库

    我不熟悉这个库。它本身是否支持密文窃取?我假设不是。因此,您有两种可能的策略。

    1. 使用 CBC 模式为除最后两个块之外的所有块调用库的解密。按照我向您描述的方式处理最后两个块。但这需要访问 CV。 API会公开这个吗?如果不是,则此策略对您来说不是一个可行的选择。

    2. 使用 ECB 模式为除最后两个块之外的所有块调用库的解密,并滚动您的 CBC 链接。相当容易实施和定义,您可以访问 CV。

    如何在 PHP 中进行异或操作

    其他人发布了此问题的答案,但目前已将其撤回。但他是对的。它看起来像是在 PHP 中对字节数组进行 XOR,逐个遍历字符,然后进行字节级 XOR。该技术显示在here

    【讨论】:

    • 哇!这真的很有帮助。 PHP 确实支持字节数组,但函数 mcrypt_decrypt 接受字符串作为参数,用于 IV、密钥和密文。此外, strlen() 计算 UTF8 字节数。所以它不会在零值字节上终止。 mcrypt_decrypt 不公开使用的 CV。关于 XOR,我对 wiki 的措辞“XOR the last ciphertext with the second last ciphertext”感到困惑。 “最后一个”和“倒数第二个”是什么意思?
    • 让密文(输入)消息由N个块组成。将索引原点设置为零,因此 c[0] 表示第一个块,而 c[1] 是第二个块。因此 c[N-1] 是“最后一个”块,而 c[N-2] 是“倒数第二个”块。
    • 其他要观察的属性: (1) 也会有精确的 N 个重构明文(输出)块; (2)最后一个块c[N-1]可能不是full size,但所有其他密文块都是full size; (3) 最后一个明文块 m[N-1] 的大小将与最后一个密文块 c[N-1] 的大小相同。
    【解决方案2】:

    我正在为 perl 寻找类似的答案。 Perl 的库仅限于 CBC 模式。以下是我使用 AES 256 CBC 模式和 CTS 方法 3 使 CTS 工作的方法。我认为这对 PHP 也可能有帮助。

    这是实际的 NIST 文档。 文件编号:NIST800-38A CBC-CS3 标题:“分组密码操作模式建议”; CBC 模式的三种密文窃取变体。 来源:http://csrc.nist.gov/publications/nistpubs/800-38a/addendum-to-nist_sp800-38A.pdf

    这是代码...

    use Crypt::CBC;
    use Crypt::Cipher::AES;
    
    my $key = pack("H*","0000000000000000000000000000000000000000000000000000000000000000");
    my $iv = pack("H*","00000000000000000000000000000000");
    my $pt = pack("H*","0000000000000000000000000000000000");
    my $ct = aes256_cbc_cts_decrypt( $key, $iv, $pt );
    
    #AES 256 CBC with CTS
    sub aes256_cbc_cts_decrypt {
        my ($key, $iv, $in) = @_;
        my $len_in_bytes = length(unpack("H*", $in)) / 2;
        my $in_idx = 0;
        my $null_iv = pack( "H32", "00000000000000000000000000000000");
        my $cipher = Crypt::CBC->new(
                        -key         => $key,
                        -iv          => $null_iv,
                        -literal_key => '1',
                        -keysize     => 32,
                        -blocksize   => 16,
                        -header      => 'none',
                        -cipher      => 'Crypt::Cipher::AES');
        my $out;
        while ( $len_in_bytes >= 16 )
        {
            my $tmp = substr($in, $in_idx, 16);
            my $outblock = $cipher->decrypt($tmp);
            if ( ( ($len_in_bytes % 16) eq 0 ) || ( $len_in_bytes > 32 ) )
            {
                $outblock = $outblock ^ $iv;
                $iv = $tmp;
            }
            $out .= $outblock;
            $in_idx += 16;
            $len_in_bytes -= 16;
        }
    
        if ($len_in_bytes) {
            my $tmp = substr($in,$in_idx,$len_in_bytes);
            my $out_idx = $in_idx - 16;
            $tmp .= substr($out,$out_idx + $len_in_bytes, 16 - $len_in_bytes);
            $out .= substr($out, $out_idx, $len_in_bytes) ^ substr($tmp, 0, $len_in_bytes);
            substr($out,$out_idx,16) = $iv ^ $cipher->decrypt($tmp);
        }
        return $out;
    }
    

    【讨论】:

      猜你喜欢
      • 2012-05-11
      • 2012-04-26
      • 2011-12-20
      • 2023-03-03
      • 1970-01-01
      • 2017-09-28
      • 2012-09-08
      • 2010-11-02
      • 1970-01-01
      相关资源
      最近更新 更多