【问题标题】:Decrypting AES and HMAC with PyCrypto使用 PyCrypto 解密 AES 和 HMAC
【发布时间】:2015-08-08 00:11:16
【问题描述】:

在获取 AES 密文进行解密时遇到了一些麻烦。

在这个特定场景中,我在客户端使用 Crypto-JS 加密数据,然后使用 PyCrypto 在 python 服务器上将其解密。

加密.js:

  var password = 'BJhtfRjKnTDTtPXUBnErKDxfkiMCOLyP';
  var data = 'mytext';

  var masterKey = CryptoJS.SHA256(password).toString();

  // Derive keys for AES and HMAC
  var length = masterKey.toString().length / 2
  var encryptionKey = masterKey.substr(0, length);
  var hmacKey = masterKey.substr(length);

  var iv = CryptoJS.lib.WordArray.random(64/8);

  var encrypted = CryptoJS.AES.encrypt(
    data,
    encryptionKey,
    {
      iv: iv,
      mode: CryptoJS.mode.CFB
    }
  );

  var concat = iv + encrypted;

  // Calculate HMAC using iv and cipher text
  var hash = CryptoJS.HmacSHA256(concat, hmacKey);

  // Put it all together
  var registrationKey = iv + encrypted + hash;

  // Encode in Base64
  var basemessage = btoa(registrationKey);

解密.py:

class AESCipher:
    def __init__(self, key):
        key_hash = SHA256.new(key).hexdigest()
        # Derive keys
        encryption_key = key_hash[:len(key_hash)/2]
        self.key = encryption_key            
        self.hmac_key = key_hash[len(key_hash)/2:]


    def verify_hmac(self, input_cipher, hmac_key):
        # Calculate hash using inputted key
        new_hash = HMAC.new(hmac_key, digestmod=SHA256)
        new_hash.update(input_cipher)
        digest = new_hash.hexdigest()

        # Calculate hash using derived key from local password
        local_hash = HMAC.new(self.hmac_key, digestmod=SHA256)
        local_hash.update(input_cipher)
        local_digest = local_hash.hexdigest()

        return True if digest == local_digest else False


    def decrypt(self, enc):
        enc = base64.b64decode(enc)
        iv = enc[:16]
        hmac = enc[60:]
        cipher_text = enc[16:60]

        # Verify HMAC using concatenation of iv + cipher like in js
        verified_hmac = self.verify_hmac((iv+cipher_text), self.hmac_key)

        if verified_hmac:
            cipher = AES.new(self.key, AES.MODE_CFB, iv)
            return cipher.decrypt(cipher_text)


password = 'BJhtfRjKnTDTtPXUBnErKDxfkiMCOLyP'

input = 'long base64 registrationKey...'

cipher = AESCipher(password)
decrypted = cipher.decrypt(input)

我成功地重新计算了 HMAC,但是当我尝试解密密码时,我得到的结果似乎是用 � 加密的。

我收到有关密文输入长度的错误,但是当我切换到 CFB 模式时,它修复了它,所以我认为这不是填充问题。

【问题讨论】:

    标签: python encryption hmac pycrypto cryptojs


    【解决方案1】:

    你的代码有很多问题。

    客户端(JavaScript):

    • AES 的块大小为 128 位,CFB 模式要求 IV 有一个完整的块。使用

      var iv = CryptoJS.lib.WordArray.random(128/8);
      
    • ivhash 变量是 WordArray 对象,但 encrypted 不是。当您通过连接它们 (+) 强制将它们转换为字符串时,ivhash 是十六进制编码的,但 encrypted 的格式是 OpenSSL 兼容格式和 Base64 编码。您需要访问ciphertext 属性来获取加密的WordArray:

      var concat = iv + encrypted.ciphertext;
      

      var registrationKey = iv + encrypted.ciphertext + hash;
      
    • registrationKey 是十六进制编码的。无需再次使用 Base64 对其进行编码并使其更加膨胀:

      var basemessage = registrationKey;
      

      如果要将十六进制编码的registrationKey 转换为base64 编码,请使用:

      var basemessage = CryptoJS.enc.Hex.parse(registrationKey).toString(CryptoJS.enc.Base64);
      
    • concat 是 IV 和密文的十六进制编码字符串,因为您通过“添加”(+)ivencrypted 强制进行字符串化。 HmacSHA256() 函数采用 WordArray 对象或字符串。当你传入一个字符串时,它会假定数据是 UTF-8 编码的,并尝试将其解码为 UTF-8。您需要自己将数据解析为 WordArray:

      var hash = CryptoJS.HmacSHA256(CryptoJS.enc.Hex.parse(concat), hmacKey);
      
    • CryptoJS.AES.encrypt()CryptoJS.HmacSHA256() 期望键是 WordArray 对象或字符串。和以前一样,如果密钥作为字符串提供,则假定为 UTF-8 编码,但这里不是这种情况。您最好自己将字符串解析为 WordArrays:

      var encryptionKey = CryptoJS.enc.Hex.parse(masterKey.substr(0, length));
      var hmacKey = CryptoJS.enc.Hex.parse(masterKey.substr(length));
      

    服务器(Python):

    • 您没有验证 verify_hmac() 中的任何内容。您使用相同的密钥对相同的数据进行两次哈希处理。您需要做的是对 IV+密文进行哈希处理,并将结果与​​您切出完整密文的哈希(称为标记或 HMAC 标记)进行比较。

      def verify_hmac(self, input_cipher, mac):
          # Calculate hash using derived key from local password
          local_hash = HMAC.new(self.hmac_key, digestmod=SHA256)
          local_hash.update(input_cipher)
          local_digest = local_hash.digest()
      
          return mac == local_digest
      

      后来在decrypt():

      verified_hmac = self.verify_hmac((iv+cipher_text), hmac)
      
    • 您需要正确切掉 MAC。硬编码的 60 是个坏主意。由于您使用的是 SHA-256,因此 MAC 的长度为 32 个字节,因此您可以这样做

      hmac = enc[-32:]
      cipher_text = enc[16:-32]
      
    • CFB模式实际上是一组类似的模式。实际模式由段大小决定。 CryptoJS 仅支持 128 位的段。所以你需要告诉 pycrypto 使用与 CryptoJS 相同的模式:

      cipher = AES.new(self.key, AES.MODE_CFB, iv, segment_size=128)
      

      如果您想使用分段大小为 8 位(默认为 pycrypto)的 CFB 模式,您可以在我的项目中的 CryptoJS 中使用修改后的 CFB 版本:Extension for CryptoJS

    完整的客户端代码:

    var password = 'BJhtfRjKnTDTtPXUBnErKDxfkiMCOLyP';
    var data = 'mytext';
    
    var masterKey = CryptoJS.SHA256(password).toString();
    var length = masterKey.length / 2
    var encryptionKey = CryptoJS.enc.Hex.parse(masterKey.substr(0, length));
    var hmacKey = CryptoJS.enc.Hex.parse(masterKey.substr(length));
    
    var iv = CryptoJS.lib.WordArray.random(128/8);
    
    var encrypted = CryptoJS.AES.encrypt(
        data,
        encryptionKey,
        {
          iv: iv,
          mode: CryptoJS.mode.CFB
        }
    );
    
    var concat = iv + encrypted.ciphertext; 
    var hash = CryptoJS.HmacSHA256(CryptoJS.enc.Hex.parse(concat), hmacKey);
    var registrationKey = iv + encrypted.ciphertext + hash;
    console.log(CryptoJS.enc.Hex.parse(registrationKey).toString(CryptoJS.enc.Base64));
    

    完整的服务器代码:

    from Crypto.Cipher import AES
    from Crypto.Hash import HMAC, SHA256
    import base64
    import binascii
    
    class AESCipher:
        def __init__(self, key):
            key_hash = SHA256.new(key).hexdigest()
            self.hmac_key = binascii.unhexlify(key_hash[len(key_hash)/2:])
            self.key = binascii.unhexlify(key_hash[:len(key_hash)/2])
    
        def verify_hmac(self, input_cipher, mac):
            local_hash = HMAC.new(self.hmac_key, digestmod=SHA256)
            local_hash.update(input_cipher)
            local_digest = local_hash.digest()
    
            return SHA256.new(mac).digest() == SHA256.new(local_digest).digest() # more or less constant-time comparison
    
        def decrypt(self, enc):
            enc = base64.b64decode(enc)
            iv = enc[:16]
            hmac = enc[-32:]
            cipher_text = enc[16:-32]
    
            verified_hmac = self.verify_hmac((iv+cipher_text), hmac)
    
            if verified_hmac:
                cipher = AES.new(self.key, AES.MODE_CFB, iv, segment_size=128)
                return cipher.decrypt(cipher_text)
            else:
                return 'Bad Verify'
    
    
    password = 'BJhtfRjKnTDTtPXUBnErKDxfkiMCOLyP'
    
    input = "btu0CCFbvdYV4B/j7hezAra6Q6u6KB8n5QcyA32JFLU8QRd+jLGW0GxMQsTqxaNaNkcU2I9r1ls4QUPUpaLPQg=="
    
    obj = AESCipher(password)
    decryption = obj.decrypt(input)
    
    print 'Decrypted message:', decryption
    

    【讨论】:

    • 首先,非常感谢您抽出宝贵的时间来写出精彩的答案!我还在学习密码学,这是一股新鲜空气。实现代码后,出现了一些错误。首先,哈希验证一直返回为无。在将结果设置为 True 以尝试下一部分之后,我仍然得到了外国编码的文本。另外,我对IV感到困惑。您在 js 中将长度加倍,但在 python 中,您仍然只抓住了它的前半部分([:16])。如果您尝试获得更多,则会引发错误,但仅获得一半没有多大意义。再次感谢!
    • (1) 我错过了客户端代码中的两个编码问题,并将它们添加到我的答案中。 (2) HMAC 验证应该不可能返回None。我不知道那里发生了什么。 (3) CryptoJS 中的128/8 部分创建一个 16 字节的 IV。当您将其编码为十六进制时,它将是 32 个 字符 长。如果您使用 Hex 到 Base64 的转换代码,那么您的 python 代码应该正确解码 Base64 编码的密文,并且 IV 应该是 16 字节长。
    • 还有更多的编码问题。我已经添加了完整的工作代码
    • 很棒的答案,但我有点担心使用 == 验证 hmac。可能容易受到定时攻击。见docs.python.org/2/library/hmac.html#hmac.compare_digest
    • @Chris 它是在 2.7a1 中添加的,它不是一个稳定的版本。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-16
    • 1970-01-01
    • 2014-01-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多