AES CBC加密红宝石流?

时间:2011-02-05 20:09:02

标签: ruby encryption openssl aes

我一直在使用红宝石中的cbc加密的一个相当标准的例子(我的目的严重受损):

def aes(m,k,t)
  (aes = OpenSSL::Cipher::Cipher.new('aes-256-cbc').send(m)).key = Digest::SHA256.digest(k)
  aes.update(t) << aes.final
end

def encrypt(key, text)
  aes(:encrypt, key, text)
end

def decrypt(key, text)
  aes(:decrypt, key, text)
end

这可以作为一个可接受的起点,但我需要能够加密大量数据流,而无需将它们加载到一大块内存中。我想一次加载一个meg,更新加密流的状态,然后继续下一个块。看看OpenSSL Cipher上的文档(获奖者很差),我希望更新调用应该只是继续数据流。但是,一个简单的测试告诉我有一些非常错误:

Length = 256
newaes = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
newaes.encrypt
newaes.key= Digest::SHA256.digest("foo")
puts Base64.encode64(newaes.update("a"*Length))
puts Base64.encode64(newaes.update("a"*Length))
puts Base64.encode64(newaes.final)

使用不同的Length值运行它不应该给我不同的流。但是,在第一次更新结束后,总会出现问题。溪流分歧。我猜测问题是由于一些莫名其妙的原因,字符串末尾的终止空('\ 0')字符被加密。毕竟,每次更新调用都返回一个字符串,该字符串是((string.length / 16)+ 1)* 16字节长,这意味着每次更新时它都在加密一个额外的字节。

我如何让OpenSSL的加密和解密在我可以传入数据块并获得相同结果的模式下运行,无论我将数据分成多大的块?

编辑:

该问题独立于base64编码。以下产生3种不同的密文结果:

require 'digest/sha2'
require 'base64'
require 'openssl'

def base64(data)
    Base64.encode64(data).chomp
end

def crypt_test(blocksize)
    newaes = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
    newaes.encrypt
    newaes.key= Digest::SHA256.digest("foo")
    plaintext = ""
    cyphertext = ""
    File.open("black_bar.jpg") do |fd|
        while not fd.eof
            data = fd.read(blocksize)
            cyphertext += data
            cyphertext += newaes.update(data)
        end
    end
    cyphertext += newaes.final
    puts base64(Digest::SHA256.digest(plaintext))
    puts base64(Digest::SHA256.digest(cyphertext))
    puts
end

crypt_test(1024)
crypt_test(512)
crypt_test(2048)

2 个答案:

答案 0 :(得分:3)

我对Ruby知之甚少。但是,您的问题看起来像填充问题。

AES / CBC按16字节的块加密数据,不少。 Padding 是关于添加几个字节,以便:

  1. 填充长度是16的倍数;
  2. 解密后,可以明确删除多余的字节。
  3. 第二个条件意味着不存在“零长度填充”(至少,不是没有采用诸如“密文窃取”之类的黑暗技巧)。必须至少有一个额外的填充字节。否则,解密器将不知道所获得的数据的结尾是否真的是一些填充,或者实际的消息恰好以某些字节“看起来像”填充结束。

    一种非常常见的填充方案是PKCS#5中指定的填充方案(参见第6.1.1节):对于长度为 n 的块( n = 16, AES),至少添加1个,最多 n 个字节;如果添加 k 字节,则它们都具有数值 k 。在解密时,只需要查看最后一个字节的数值就可以知道添加了多少填充字节。 PKCS#5填充方案意味着您观察到的行为: m 字节的加密产生 n *((m / n)+1)输出字节。

    如果您的呼叫确实在每个update添加了PKCS#5填充,那么您可以通过删除它们返回的最后16个字节来从中恢复。您还必须为下一个update调用重置IV,以便可以简单地追加下一个update调用返回的内容。说到这一点,我的代码中没有看到关于IV的内容,这很可疑。 CBC模式需要每个消息的新随机IV(用“足够强”的生成器选择);然后必须将IV与加密消息一起传输(无论谁解密数据都需要它; IV可以“明确地”发送。)

    如果你知道CBC如何工作,上面的段落应该更清楚。 Wikipedia有很好的原理图。

答案 1 :(得分:2)

这是你的问题:

Length = 256
newaes = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
newaes.encrypt
newaes.key= Digest::SHA256.digest("foo")
s1 = newaes.update("a"*Length)
s2 = newaes.update("a"*Length)
s3 = newaes.final
puts Base64.encode64(s1 + s2 + s3)

这将输出完全相同的base64,就好像你将两个更新压缩成一个。

您遇到了base64编码的“对齐”问题。 Base64编码一次需要3个字节,并将它们转换为4个字节。如果你给它一个不是3的倍数的字节数,它会用'='符号来填充。

这意味着如果你有两个连续的编码运行不是3个字节长的倍数,然后在一个编码运行中编码完全相同的字节序列,你将获得不同的base64输出。第二次编码运行不是“对齐”,就像数据是第一次编码运行的一部分一样。以下是一些例子:

这里,数据是3的倍数。编码器的两次运行产生base64序列,这些序列可以连接在一起,产生与串联字符串上的一次编码器运行大致相同的序列。

> Base64.encode64('abc')
=> "YWJj\n"
> Base64.encode64('def')
=> "ZGVm\n"
> Base64.encode64('abcdef')
=> "YWJjZGVm\n"

这里数据被分成4个字节的序列,4不是3的倍数。编码器的两次运行的串联与连接的两个字符串的编码不同。

> Base64.encode64('abcd')
=> "YWJjZA==\n"
> Base64.encode64('efgh')
=> "ZWZnaA==\n"
> Base64.encode64('abcdefgh')
=> "YWJjZGVmZ2g=\n"