如何使用Elixir中的AES CBC 128进行加密和解密

rez*_*zam 5 ruby erlang elixir phoenix-framework

我在Rails中有一个应用程序,它有以下方法来加密和解密文本并与Java客户端通信.

def encrypt(string, key)
    cipher = OpenSSL::Cipher::AES.new(128, :CBC)
    cipher.encrypt
    cipher.padding = 1
    cipher.key = hex_to_bin(Digest::SHA1.hexdigest(key)[0..32])
    cipher_text = cipher.update(string)
    cipher_text << cipher.final
    return bin_to_hex(cipher_text).upcase
end

def decrypt(encrypted, key)
    encrypted = hex_to_bin(encrypted.downcase)
    cipher = OpenSSL::Cipher::AES.new(128, :CBC)
    cipher.decrypt
    cipher.padding = 1
    cipher.key = hex_to_bin(Digest::SHA1.hexdigest(key)[0..32])
    d = cipher.update(encrypted)
    d << cipher.final
rescue Exception => exc
end

def hex_to_bin(str)
    [str].pack "H*"
end

def bin_to_hex(str)
    str.unpack('C*').map{ |b| "%02X" % b }.join('')
end
Run Code Online (Sandbox Code Playgroud)

我需要在Elixir中为凤凰框架做同样的事情.由于我是Elixir的新手,我找不到办法.我发现Elixir使用了Erlang的:crypto模块.在文档中没有AES CBC加密方法.

mat*_*att 13

block_encrypt/4功能从二郎密码模块是你想要的功能.与Ruby OpenSSL绑定不同,Erlang代码不处理填充,因此您需要在加密之前自己执行填充(并在解密后删除它).

但是,除非这只是一个用于学习目的的玩具应用程序,否则我建议不要自己做这种加密,如果你可以避免它.相反,你应该找到一个更高级别的API来处理你可能出错的各种细节.我已经在下面列出了您的代码的一些潜在问题,以及建议您做什么.


OpenSSL使用的填充(有时称为PKCS7填充)非常简单.首先,您需要计算出需要添加到数据中的字节数,以使长度成为块大小的倍数(AES为16).然后,您只需将该值的多个字节添加到结尾.例如,如果您的数据长度为14个字节,则需要添加两个字节,每个字节的值为0x02(每个值为2的2个字节).请注意,您总是添加填充,因此,如果您的数据已经再添加16字节的倍数另一个 16个字节(所有数值为0x10).

要剥离填充,只需查看最后一个字节的值并从末尾删除那么多字节(您应该检查填充是否正确,即所有字节都具有预期值).

这是Elixir中的一个简单实现(可能有更好/更清晰/更惯用的方式来执行此操作):

# These will need to be in a module of course
def pad(data, block_size) do
  to_add = block_size - rem(byte_size(data), block_size)
  data <> to_string(:string.chars(to_add, to_add))
end

def unpad(data) do
  to_remove = :binary.last(data)
  :binary.part(data, 0, byte_size(data) - to_remove)
end
Run Code Online (Sandbox Code Playgroud)

您现在可以将这些与:crypto.block_encrypt函数一起使用,以获得像您的Ruby代码一样的AES CBC加密:

# BAD, don't do this!
# This is just to reproduce your code, where you are not using 
# an initialisation vector.
@zero_iv to_string(:string.chars(0, 16))
@aes_block_size 16

def encrypt(data, key) do
  :crypto.block_encrypt(:aes_cbc128, key, @zero_iv, pad(data, @aes_block_size))
end

def decrypt(data, key) do
  padded = :crypto.block_decrypt(:aes_cbc128, key, @zero_iv, data)
  unpad(padded)
end
Run Code Online (Sandbox Code Playgroud)

一些问题

以下是您的代码可能存在的一些问题.这不是一个详尽的列表,只是我注意到的一些事情(我不是加密专家).

  1. 没有身份验证 除非您在显示的代码之前以其他方法检查身份验证,否则您没有对消息进行任何身份验证.这非常糟糕.您正在暴露自己潜在的填充oracle攻击(攻击者可以解密消息)和诸如位翻转攻击之类的东西,攻击者可以发送特殊修改的消息,代码可能不会识别为坏,并导致一些不需要的操作地点.

    你应该使用像HMAC这样的东西.但即使您决定使用HMAC,仍然需要解决几个问题.HMAC密钥来自哪里?我们可以使用相同的密钥进行加密和身份验证吗?我们计算明文或密文的HMAC吗?它是否应该涵盖IV?

  2. 没有初始化矢量.CBC模式应该使用初始化矢量或IV.在Ruby OpenSSL绑定中,如果你没有指定一个,它只使用零字节(这就是为什么我们需要@zero_iv在上面的代码中创建.每个消息应该有自己的IV.这可能只是一个随机的字节序列,并且不需要保密(它可以只是被发送到密文之前).

  3. 密钥生成薄弱.我可能错了这个,但是因为你计算提供的key参数的SHA1哈希值用作加密/解密密钥,所以它表明这个参数实际上是一个密码.如果是这种情况,那么您应该使用更好的密钥派生函数(如果不是,那么散列的目的是什么?).如果您使用简单的人类来记住密码(或单个哈希值),您可能容易受到暴力攻击,攻击者会尝试使用大量字典单词作为密钥.

    您应该使用正确的密钥派生函数,例如PBKDF2.即便如此,您仍然会遇到并发症,因为您可能需要两个密钥(加密和身份验证),因此您需要确定如何生成它们.


用什么代替

如果可能,您应该寻找一个考虑这些因素的更高级别的库,并提供更简单的API.我推荐Libsodium,它有许多语言的绑定,包括Ruby,Elixir,Erlang和Java/Android.