Попытка расшифровать строку с помощью openssl/golang, которая была зашифрована в рельсах

Я пытаюсь расшифровать строку, которая была зашифрована в моем проекте rails. Вот как я шифрую данные:

def encrypt_text(text_To_encrypt)
        # 0. generate the key using command openssl rand -hex 16 on linux machines
        # 1. Read the secret from config
        # 2. Read the salt from config
        # 3. Encrypt the data
        # 4. return the encypted data
        # Ref: http://www.monkeyandcrow.com/blog/reading_rails_how_does_message_encryptor_work/
        secret = Rails.configuration.miscconfig['encryption_key']
        salt = Rails.configuration.miscconfig['encryption_salt']
        key = ActiveSupport::KeyGenerator.new(secret).generate_key(salt, 32)
        crypt = ActiveSupport::MessageEncryptor.new(key)
        encrypted_data = crypt.encrypt_and_sign(text_To_encrypt)
        encrypted_data
end

Теперь проблема в том, что я не могу расшифровать его с помощью openssl. Он просто показывает плохое магическое число. Как только я сделаю это в открытом ssl, мой план состоит в том, чтобы расшифровать его в golang.

Вот как я пытался расшифровать его с помощью openssl:

openssl enc -d -aes-256-cbc -salt -in encrypted.txt -out decrypted.txt -d -pass pass:<the key given in rails> -a

Это просто показывает плохое магическое число


person defiant    schedule 26.03.2019    source источник
comment
pass.txt совпадает с encryption_key?   -  person Vasiliy Faronov    schedule 26.03.2019
comment
Да, потому что я не знаю, что указать в качестве пароля.   -  person defiant    schedule 27.03.2019
comment
@VasiliyFaronov Я понял, что даю пароль вместо ключа в рельсах. Ключ генерируется рельсами.   -  person defiant    schedule 27.03.2019
comment
Какие версии Ruby и Rails вы используете для шифрования?   -  person BoraMa    schedule 28.03.2019
comment
@BoraMa Рельсы 5.0.2   -  person defiant    schedule 28.03.2019


Ответы (1)


Попытка расшифровать данные, зашифрованные в другой системе, не сработает, если вы не знаете и не имеете дело со множеством сложных деталей того, как обе системы выполняют криптографию. Хотя и Rails, и инструмент командной строки openssl под капотом используют библиотеки OpenSSL для своих криптографических операций, они оба используют его по-своему, не взаимодействуя напрямую.

Если вы внимательно посмотрите на две системы, вы увидите, что, например:

  • Шифровальщик сообщений Rails не только шифрует сообщение, но и подписывает его
  • Шифровальщик Rails использует Marshal для сериализации входных данных.
  • инструмент openssl enc ожидает зашифрованные данные в отдельном формате файла с заголовком Salted__<salt> (поэтому вы получаете сообщение плохой магический номер от openssl)
  • инструмент openssl должен быть правильно настроен для использования тех же шифров, что и шифратор Rails и генератор ключей, поскольку значения по умолчанию openssl отличаются от значений по умолчанию Rails.
  • конфигурация шифров по умолчанию значительно изменилась по сравнению с Rails 5.2.

С этой общей информацией мы можем взглянуть на практический пример. Он протестирован в Rails 4.2, но должен одинаково работать и в Rails 5.1.

Анатомия сообщения, зашифрованного Rails

Позвольте мне начать с немного измененного кода, который вы представили. Единственными изменениями являются установка статических значений password и salt и печать большого количества отладочной информации:

def encrypt_text(text_to_encrypt)
  password = "password" # the password to derive the key
  salt = "saltsalt" # salt must be 8 bytes

  key = ActiveSupport::KeyGenerator.new(password).generate_key(salt, 32)

  puts "salt (hexa) = #{salt.unpack('H*').first}" # print the saltin HEX
  puts "key (hexa) = #{key.unpack('H*').first}" # print the generated key in HEX

  crypt = ActiveSupport::MessageEncryptor.new(key)
  output = crypt.encrypt_and_sign(text_to_encrypt)
  puts "output (base64) = #{output}"
  output
end

encrypt_text("secret text")

Когда вы запустите это, вы получите что-то вроде следующего вывода:

salt (hexa) = 73616c7473616c74
key (hexa) = 196827b250431e911310f5dbc82d395782837b7ae56230dce24e497cf07b6518
output (base64) = SGRTUXYxRys1N1haVWNpVWxxWTdCMHlyMk15SnQ0dWFBOCt3Z0djWVdBZz0tLTkrd1hBNWJMVm9HcnptZ3loOG1mNHc9PQ==--80d091e8799776113b2c0efd1bf75b344bf39994

Последняя строка (вывод метода encrypt_and_sign) представляет собой комбинацию двух частей, разделенных -- (см. источник):

  1. зашифрованное сообщение (в кодировке Base64) и
  2. подпись сообщения (в кодировке Base64).

Подпись не важна для шифрования, поэтому давайте посмотрим на первую часть — давайте расшифруем ее в консоли Rails:

> Base64.strict_decode64("SGRTUXYxRys1N1haVWNpVWxxWTdCMHlyMk15SnQ0dWFBOCt3Z0djWVdBZz0tLTkrd1hBNWJMVm9HcnptZ3loOG1mNHc9PQ==")
=> "HdSQv1G+57XZUciUlqY7B0yr2MyJt4uaA8+wgGcYWAg=--9+wXA5bLVoGrzmgyh8mf4w=="

Вы можете видеть, что декодированное сообщение снова состоит из двух частей в кодировке Base64, разделенных -- (см. источник):

  1. само зашифрованное сообщение
  2. вектор инициализации, используемый в шифровании

Шифровальщик сообщений Rails по умолчанию использует шифр aes-256-cbc (обратите внимание, что он изменился с версии Rails 5.2). Для этого шифра требуется вектор инициализации, который случайным образом генерируется Rails и должен присутствовать в зашифрованном выводе, чтобы мы могли использовать его вместе с ключом для расшифровки сообщения.

Кроме того, Rails шифрует входные данные не как простой текст, а как сериализованную версию данных, используя сериализатор Marshal по умолчанию (источник). Если бы мы расшифровали такое сериализованное значение с помощью openssl, мы все равно получили бы слегка искаженную (сериализованную) версию исходных текстовых данных. Вот почему будет более уместно отключить сериализацию при шифровании данных в Rails. Это можно сделать, передав параметр методу шифрования:

  # crypt = ActiveSupport::MessageEncryptor.new(key)
  crypt = ActiveSupport::MessageEncryptor.new(key, serializer: ActiveSupport::MessageEncryptor::NullSerializer)

Повторный запуск кода дает результат, который немного короче, чем в предыдущей версии, потому что зашифрованные данные теперь не сериализованы:

salt (hexa) = 73616c7473616c74
key (hexa) = 196827b250431e911310f5dbc82d395782837b7ae56230dce24e497cf07b6518
output (base64) = SUlIWFBjSXRUc0JodEMzLzhXckJzUT09LS1oZGtPV1ZRc2I5Wi8zOG01dFNOdVdBPT0=--58bbaf983fd20459062df8b6c59eb470311cbca9

Наконец, мы должны узнать некоторую информацию о процедуре получения ключа шифрования. исходник сообщает нам, что KeyGenerator использует алгоритм pbkdf2_hmac_sha1 с 2**16 = 65536 итерациями для получения ключа из пароля/секрета.

Анатомия openssl зашифрованного сообщения

Теперь аналогичное расследование необходимо провести на стороне openssl, чтобы узнать подробности процесса его расшифровки. Во-первых, если вы зашифруете что-либо с помощью инструмента openssl enc, вы обнаружите, что выходные данные имеют отличный формат:

Salted__<salt><encrypted_message>

Он начинается с магической строки Salted__, затем следует соль (в шестнадцатеричном формате) и, наконец, зашифрованные данные. Чтобы иметь возможность расшифровать любые данные с помощью этого инструмента, мы должны преобразовать наши зашифрованные данные в тот же формат.

Инструмент openssl использует EVP_BytesToKey (см. source), чтобы получить ключ по умолчанию, но его можно настроить для использования алгоритм pbkdf2_hmac_sha1 с использованием параметров -pbkdf2 и -md sha1. Количество итераций можно установить с помощью опции -iter.

Как расшифровать сообщение, зашифрованное Rails, в openssl

Итак, наконец, у нас достаточно информации, чтобы попытаться расшифровать сообщение, зашифрованное Rails, в openssl.

Сначала мы должны снова декодировать первую часть зашифрованного Rails вывода, чтобы получить зашифрованные данные и вектор инициализации:

> Base64.strict_decode64("SUlIWFBjSXRUc0JodEMzLzhXckJzUT09LS1oZGtPV1ZRc2I5Wi8zOG01dFNOdVdBPT0=")
=> "IIHXPcItTsBhtC3/8WrBsQ==--hdkOWVQsb9Z/38m5tSNuWA=="

Теперь давайте возьмем IV (вторую часть) и преобразуем его в форму шестнадцатеричной строки, так как это форма, которая нужна openssl:

> Base64.strict_decode64("hdkOWVQsb9Z/38m5tSNuWA==").unpack("H*").first
=> "85d90e59542c6fd67fdfc9b9b5236e58"  # the initialization vector in hex form

Теперь нам нужно взять данные, зашифрованные с помощью Rails, и преобразовать их в формат, который openssl распознает, то есть добавить к ним магическую строку и соль и снова закодировать их в Base64:

> Base64.strict_encode64("Salted__" + "saltsalt" + Base64.strict_decode64("IIHXPcItTsBhtC3/8WrBsQ=="))
=> "U2FsdGVkX19zYWx0c2FsdCCB1z3CLU7AYbQt//FqwbE=" # encrypted data suitable for openssl

Наконец, мы можем создать команду openssl для расшифровки данных:

$ echo  "U2FsdGVkX19zYWx0c2FsdCCB1z3CLU7AYbQt//FqwbE=" | 
> openssl enc -aes-256-cbc -d -iv 85d90e59542c6fd67fdfc9b9b5236e58 \
>   -pass pass:password -pbkdf2 -iter 65536 -md sha1 -a
secret text

И вуаля, мы успешно расшифровали исходное сообщение!

Параметры openssl следующие:

  • -aes-256-cbc устанавливает тот же шифр, который Rails использует для шифрования.
  • -d означает расшифровку
  • -iv передает вектор инициализации в виде шестнадцатеричной строки
  • -pass pass:password устанавливает пароль, используемый для получения ключа шифрования, на «пароль».
  • -pbkdf2 и -md sha1 задают тот же алгоритм получения ключа, который используется в Rails (pbkdf2_hmac_sha1)
  • -iter 65536 задает такое же количество итераций для получения ключа, как это было сделано в Rails.
  • -a позволяет работать с зашифрованными данными в кодировке Base64 - нет необходимости обрабатывать необработанные байты в файлах

По умолчанию openssl читает из STDIN, поэтому мы просто передаем зашифрованные данные (в правильном формате) в openssl с помощью эха.

отладка

В случае, если у вас возникнут проблемы при расшифровке с помощью openssl, полезно добавить в командную строку параметр -P, который выводит отладочную информацию о параметрах шифра/ключа:

$ echo ... | openssl ... -P
salt=73616C7473616C74
key=196827B250431E911310F5DBC82D395782837B7AE56230DCE24E497CF07B6518
iv =85D90E59542C6FD67FDFC9B9B5236E58

Значения salt, key и iv должны соответствовать значениям отладки, напечатанным исходным кодом в методе encrypt_text, напечатанном выше. Если они разные, вы знаете, что делаете что-то не так...

Теперь, я думаю, вы можете ожидать подобных проблем при попытке расшифровать сообщение в ходу, но я думаю, что у вас есть несколько хороших указателей, чтобы начать.

person BoraMa    schedule 28.03.2019
comment
святая корова, большое спасибо за подробное описание. Действительно ценю это. У меня есть один вопрос, как сериализация данных меняет данные? - person defiant; 29.03.2019
comment
Просто посмотрите сами: откройте консоль Rails и сравните текст с Marshal.dump("some text"). Вы увидите, что у последнего есть еще несколько байтов на выходе, которые обозначают тип данных (вероятно). См. документы. Я только что нашел этот загрузчик Ruby-упорядоченных данных в Go, поэтому, если вам нужно зашифровать что-то более сложное, чем строки, вам это может понадобиться (и сериализация). - person BoraMa; 29.03.2019
comment
Спасибо, теперь я знаю, что дополнительная информация обозначает тип данных. Мне было просто любопытно, как эти дополнительные символы оказались там после сериализации данных. - person defiant; 29.03.2019
comment
Да, формат упорядоченных данных является внутренним и плохо документирован, но, скорее всего, он обозначает типы данных. Это позволяет сериализатору поддерживать любые данные, а не только строки. - person BoraMa; 29.03.2019