我一直在使用红宝石中的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)
答案 0 :(得分:3)
我对Ruby知之甚少。但是,您的问题看起来像填充问题。
AES / CBC按16字节的块加密数据,不少。 Padding 是关于添加几个字节,以便:
第二个条件意味着不存在“零长度填充”(至少,不是没有采用诸如“密文窃取”之类的黑暗技巧)。必须至少有一个额外的填充字节。否则,解密器将不知道所获得的数据的结尾是否真的是一些填充,或者实际的消息恰好以某些字节“看起来像”填充结束。
一种非常常见的填充方案是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"