使用 PyCrypto 解密 AES 和 HMAC
Decrypting AES and HMAC with PyCrypto
在解密 AES 密文时遇到一些麻烦。
在这种特殊情况下,我在客户端使用 Crypto-JS 加密数据,然后在 python 服务器上使用 PyCrypto 将其解密。
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,但是当我尝试然后解密密码时,我得到的结果似乎是用 �'s 加密的。
我收到有关密文输入长度的错误,但当我切换到 CFB 模式时修复了它,所以我认为这不是填充问题。
你的代码有很多问题。
客户(JavaScript):
AES 的块大小为 128 位,CFB 模式需要 IV 的完整块。使用
var iv = CryptoJS.lib.WordArray.random(128/8);
iv
和hash
变量是WordArray对象,但encrypted
不是。当您通过连接它们 (+
) 强制将它们转换为字符串时,iv
和 hash
是十六进制编码的,但 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 和密文的十六进制编码字符串,因为您通过 "adding" (+
) iv
强制进行了字符串化和 encrypted
。 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 位的 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
在解密 AES 密文时遇到一些麻烦。
在这种特殊情况下,我在客户端使用 Crypto-JS 加密数据,然后在 python 服务器上使用 PyCrypto 将其解密。
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,但是当我尝试然后解密密码时,我得到的结果似乎是用 �'s 加密的。
我收到有关密文输入长度的错误,但当我切换到 CFB 模式时修复了它,所以我认为这不是填充问题。
你的代码有很多问题。
客户(JavaScript):
AES 的块大小为 128 位,CFB 模式需要 IV 的完整块。使用
var iv = CryptoJS.lib.WordArray.random(128/8);
iv
和hash
变量是WordArray对象,但encrypted
不是。当您通过连接它们 (+
) 强制将它们转换为字符串时,iv
和hash
是十六进制编码的,但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 和密文的十六进制编码字符串,因为您通过 "adding" (+
)iv
强制进行了字符串化和encrypted
。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 位的 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