【问题标题】:how to encrypt and decrypt with AES CBC 128 in Elixir如何在 Elixir 中使用 AES CBC 128 进行加密和解密
【发布时间】:2021-03-16 17:46:25
【问题描述】:

我在 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

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

【问题讨论】:

  • :crypto.block_encrypt/4 with first argument = :aes_cbc128 可能是你想要的,但我找不到在:crypto 中设置padding = 1 的等价物。见erlang.org/doc/man/crypto.html#block_encrypt-4
  • @Dogbert 它看起来像 Erlang 的加密库 explicitly disables padding,这似乎有点奇怪,因为这是 OpenSSL“免费”做的事情,现在用户必须自己做填充。 (它也有自己的check for the data being a multiple of 16 bytes)。
  • @matt,所以这意味着我应该更改我的android客户端中的加密方法,对吧?因为我在 java 中的数据不是 16 字节的倍数。

标签: ruby erlang elixir phoenix-framework


【解决方案1】:

来自 Erlang 加密模块的 block_encrypt/4 function 是您想要的功能。与 Ruby OpenSSL 绑定不同,Erlang 代码不处理填充,因此您需要在加密前自行处理(并在解密后将其删除)。

注意:从 Erlang v23 开始,block_encrypt/4block_decrypt/4 函数(以及它们的 /3 姐妹)已被弃用,并将从 Erlang v24 中的 Erlang crypto 模块中删除.取代它们的新 API 函数是 crypto_one_time/4crypto_one_time/5,这些函数应该用于所有新的 Erlang/Elixir 程序。新的 API 函数支持 IV 和对旧函数的其他改进。

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


OpenSSL 使用的填充(有时称为 PKCS7 填充)相当简单。首先,您需要计算出需要添加到数据中的字节数,以使长度成为块大小的倍数(AES 为 16)。然后,您只需将该值的那么多字节添加到末尾。例如,如果您的数据长度为 14 个字节,那么您需要添加两个字节,并且每个字节的值都为 0x02(每个字节的值为 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

您现在可以将这些与 :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

一些问题

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

  1. 没有身份验证。除非您在显示代码之前以另一种方法检查身份验证,否则您不会对消息进行任何身份验证。这是very bad。您将自己暴露于潜在的padding oracle attacks(攻击者可以在其中解密消息)和bit-flipping attacks 之类的东西,攻击者可以在其中发送经过特殊修改的消息,您的代码可能不会将其识别为错误,并导致发生一些不希望的操作.

您应该使用HMAC 之类的东西。但即使您决定使用 HMAC,仍有几个问题需要解决。 HMAC 密钥从何而来?我们可以使用相同的密钥进行加密和身份验证吗?我们是在明文还是密文上计算 HMAC?是否也应涵盖静脉注射?

  1. 没有初始化向量。 CBC 模式应使用initialisation vector, or IV。在 Ruby OpenSSL 绑定中,如果您没有指定一个,它只使用零字节(这就是我们需要在上面的代码中创建 @zero_iv 的原因。每条消息都应该有自己的 IV。这可以只是一系列随机字节,并且不需要保密(它可以在密文之前发送)。

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

    您应该使用正确的密钥派生函数,例如 PBKDF2。即使那样,您仍然会遇到麻烦,因为您可能需要 两个 密钥(加密和身份验证),因此您需要弄清楚如何生成它们。


改用什么

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

【讨论】:

  • 只有当填充错误被报告给调用者时,填充预言才可能发生。或使用非常量时间比较方法等不良身份验证。 Libsodium 的问题是选项有限,因此互操作性成为问题。
【解决方案2】:

我建议不要直接使用 CBC 模式,而是使用 GCM 模式,因为这也会提供身份验证。

在 Elixir 中(用于 256 位 AES 密钥)

# Gen once (see also https://hexdocs.pm/plug/Plug.Crypto.KeyGenerator.html#content)
k = :crypto.strong_rand_bytes(32)

# Gen every time you encrypt a message
iv = :crypto.strong_rand_bytes(32)
{ct, tag} = :crypto.block_encrypt(:aes_gcm, k, iv, {"AES128GCM", msg})
payload = Base.encode16(iv <> tag <> ct)

解密:

<<iv::binary-32, tag::binary-16, ct::binary>> = Base.decode16!(payload)
:crypto.block_decrypt(:aes_gcm, k, iv, {"AES128GCM", ct, tag})

【讨论】:

  • 很好的例子。尽管从这个问题security.stackexchange.com/questions/179273/… 看来,例如,AAD 通常是针对特定应用程序上下文的更具体的东西(例如用户的 ID 或加密数据的目的),而不是诸如 @ 之类的通用内容987654324@.
  • 可以说,是的!但 AAD 可以是任何东西。在此示例中,我使用了 AAD,以便解密器可以知道使用的方案并可以正确解密。这实际上取决于应用程序,但用户 ID 将是一个常见的用例。
【解决方案3】:

这是我用于 ECB 的,CBC 应该与在累加器中传递前一个分组密码的附加需要相同。不要忘记您还需要编写一个函数来将术语填充到 16 字节块(ruby 似乎自动执行此操作)。

Key = "12345678"
AES_ECB_Encrypt = 
    fun Crypt(<<Block:16/binary, Rest/binary>>, Acc) ->
            NewAcc = erlang:iolist_to_binary( [Acc, crypto:block_encrypt(aes_ecb, Key, Block)] ),
            Crypt(Rest, NewAcc);
        Crypt(_, Acc) ->
            Acc
end,
AES_ECB_Encrypt(<<"hello00000000000">>, <<>>)

【讨论】:

    【解决方案4】:

    JOSE 包中的 JOSE.JWA 组件具有 block_decrypt/4block_encrypt/4 函数。

    iex> JOSE.JWA.crypto_supports()
    [ciphers: [aes_cbc: 128, aes_cbc: 192, aes_cbc: 256, aes_ecb: 128, aes_ecb: 192,
      aes_ecb: 256, aes_gcm: 128, aes_gcm: 192, aes_gcm: 256,
      chacha20_poly1305: 256],
     hashs: [:md5, :poly1305, :sha, :sha256, :sha384, :sha512, :shake256],
     public_keys: [:ec_gf2m, :ecdh, :ecdsa, :ed25519, :ed25519ph, :ed448, :ed448ph,
      :rsa, :x25519, :x448], rsa_crypt: [:rsa1_5, :rsa_oaep, :rsa_oaep_256],
     rsa_sign: [:rsa_pkcs1_padding, :rsa_pkcs1_pss_padding]]
    

    【讨论】:

      【解决方案5】:

      感谢@matt,我在 Elixir 中写下了我的 AES_ECB

      希望对你有帮助,CBC应该也一样。

        def encrypt(data, key) do
          :crypto.block_encrypt(:aes_ecb, key, pad(data, @aes_block_size))
        end
      
        # PKCS5Padding
        defp pad(data, block_size) do
          to_add = block_size - rem(byte_size(data), block_size)
          data <> :binary.copy(<<to_add>>, to_add)
        end
      
        def decrypt(data, key) do
          padded = :crypto.block_decrypt(:aes_ecb, key, data)
          unpad(padded)
        end
      
        defp unpad(data) do
          to_remove = :binary.last(data)
          :binary.part(data, 0, byte_size(data) - to_remove)
        end
      

      【讨论】:

        猜你喜欢
        • 2013-08-11
        • 1970-01-01
        • 2020-11-29
        • 1970-01-01
        • 2018-04-04
        • 1970-01-01
        • 2019-08-17
        • 2020-04-15
        • 2021-06-25
        相关资源
        最近更新 更多