Расшифровка AES и HMAC с помощью PyCrypto

Возникли небольшие проблемы с расшифровкой шифрованного текста AES.

В этом конкретном сценарии я шифрую данные на стороне клиента с помощью Crypto-JS и расшифровываю их обратно на сервере Python с помощью PyCrypto.

шифровать.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, это исправило это, поэтому я не думаю, что это проблема заполнения.


person internetwhy    schedule 08.08.2015    source источник


Ответы (1)


В вашем коде много проблем.

Клиент (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 и зашифрованного текста, потому что вы принудительно закодировали строку, «добавив» (+) 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));
    

Сервер (Питон):

  • Вы ничего не проверяете в 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)
    

    Если вы хотите использовать режим CFB с размером сегмента 8 бит (по умолчанию pycrypto), вы можете использовать модифицированную версию CFB в CryptoJS из моего проекта: Расширение для 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
person Artjom B.    schedule 08.08.2015
comment
Во-первых, большое спасибо, что нашли время написать отличный ответ! Я все еще изучаю криптографию, и это был глоток свежего воздуха. Однако после реализации кода возникли некоторые ошибки. Во-первых, проверка хэша продолжала возвращаться как None. После того, как я просто установил для результата значение True, чтобы попробовать следующую часть, я все равно получил иностранный закодированный текст. Кроме того, меня смутил IV. Вы удвоили длину в js, но в python вы по-прежнему получаете только первую ее половину ([:16]). Ошибка выдается, если вы пытаетесь получить больше, но не имеет особого смысла получать только половину. Еще раз спасибо! - person internetwhy; 08.08.2015
comment
(1) Я пропустил две проблемы с кодировкой в ​​клиентском коде и добавил их в свой ответ. (2) Проверка HMAC не должна возвращать None. Я не знаю, что там происходит. (3) Часть 128/8 в CryptoJS создает 16-байтовый IV. Когда вы закодируете его в шестнадцатеричный формат, он будет состоять из 32 символов. Если вы используете код преобразования Hex в Base64, ваш код Python должен правильно декодировать зашифрованный текст в кодировке Base64, а IV должен иметь длину 16 байтов. - person Artjom B.; 08.08.2015
comment
Еще раз спасибо за ответ! До сих пор нет кубиков почему-то. Мне было интересно узнать о concat в js и о том, почему у него нет encrypted.ciphertext, как у registrationKey. Разве это не нужно? Также результат None был на мне. Я также решил отказаться от кодировки base64, так как вы сказали, что это просто раздувание. Большое спасибо еще раз за вашу большую помощь. Я чувствую, что это действительно близко. Я объединил текущий код в pastebin, чтобы сделать его немного более организованным. Javascript: pastebin.com/R4Ni0MpC, Python: pastebin.com/N6vcVq45 - person internetwhy; 08.08.2015
comment
Проблем с кодировкой было больше. Я добавил полный рабочий код - person Artjom B.; 08.08.2015
comment
Я не могу тебя отблагодарить! Вы сделали все возможное для меня, и я только хотел бы дать вам бонусные баллы. Я играл с unhexlify, но не применял его к клавишам. Еще раз спасибо. Вы выигрываете день. - person internetwhy; 08.08.2015
comment
Теперь мне просто нужно выяснить расшифровку javascript, лол. - person internetwhy; 08.08.2015
comment
Да, это немного сложнее, потому что WordArray не поддерживает срез. Обходной путь заключается в том, чтобы работать с шестнадцатеричными строками и преобразовывать их фрагменты в IV, зашифрованный текст и MAC; или нарезать свойство words в зашифрованном тексте: iv = CryptoJS.lib.WordArray.create(ciphertext.words.slice(0, 4)). - person Artjom B.; 08.08.2015
comment
фантастический ответ, но меня немного беспокоит проверка того, что hmac находится с ==. Может быть уязвим для атаки по времени. См. docs.python.org/2/library/hmac.html#hmac. .compare_digest - person Chris; 21.01.2017
comment
@ Крис Ты совершенно прав. Поскольку это предназначено для совместимости с большинством версий Python и PyCrypto, это более ручной процесс обеспечения сравнения с постоянным временем. Обратите внимание, что новейшая версия PyCrypto также реализует функцию проверки с постоянным временем, которая еще не задокументирована. Для этого есть олдскульное решение... - person Artjom B.; 21.01.2017
comment
@ArtjomB. Я вижу сравнение hmac в исходном коде PyCrypto (github. com/dlitz/pycrypto/blob/master/lib/Crypto/Hash/HMAC.py), но на самом деле я не вижу его при импорте, что странно. См. здесь: gist.github.com/chrisdl/eeed99d7a9edc4f1b2e0df60d00d6fca В итоге я использовал встроенный hmac проверьте, что было добавлено в python 2.7.7 - person Chris; 23.01.2017
comment
@Chris Он был добавлен в 2.7a1, который не является стабильным выпуском. - person Artjom B.; 23.01.2017