用PyCrypto解密AES和HMAC

时间:2015-08-08 00:11:17

标签: python encryption hmac pycrypto cryptojs

在获取AES密码文本以解密时遇到一些麻烦。

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

encrypt.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);

decrypt.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,但是当我尝试然后解密密码时,我得到了一些似乎用结果中的encrypted&#s加密的东西。

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

1 个答案:

答案 0 :(得分:7)

您的代码存在许多问题。

客户端(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和密文的十六进制编码字符串,因为您强制进行字符串化"添加" (+ivencryptedHmacSHA256()函数接受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位的CFB模式(默认为pycrypto),您可以在我的项目中使用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