使用openssl/golang尝试解密使用rails加密的字符串

4

我正在尝试解密一个字符串,该字符串已在我的 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解密它。它只显示错误的魔数。一旦我在openssl中这样做,我的计划就是在golang中解密它。
以下是我尝试使用openssl解密的方式:
openssl enc -d -aes-256-cbc -salt -in encrypted.txt -out decrypted.txt -d -pass pass:<the key given in rails> -a

这只是显示了错误的幻数。

1
“pass.txt” 和 “encryption_key” 是相同的吗? - Vasiliy Faronov
是的,那是因为我不知道该设置什么密码。 - defiant
@VasiliyFaronov 我明白我在Rails中提供的是密码而不是密钥。密钥是由Rails生成的。 - defiant
你在加密时使用的是哪个Ruby和Rails版本? - Matouš Borák
@BoraMa Rails 5.0.2 - defiant
1个回答

5
尝试解密在不同系统中加密的数据,除非了解并处理两个系统如何进行密码学的复杂细节,否则将无法工作。虽然Rails和openssl命令行工具都在其密码操作下使用OpenSSL库,但它们使用自己独特的方式,不直接可互操作。
如果您仔细查看两个系统,例如:
- Rails消息加密器不仅加密消息,而且还对其进行签名。 - Rails加密器使用Marshal序列化输入数据。 - openssl enc工具期望以不同的文件格式和Salted__标题接收加密数据(这就是为什么您从openssl收到bad magic number消息)。 - 必须正确配置openssl工具以使用与Rails加密器和密钥生成器相同的密码,因为openssl默认值与Rails默认值不同。 - 默认密码配置自Rails 5.2以来发生了重大变化。
有了这个通用信息,我们可以看一下一个实际的例子。它在Rails 4.2中测试过,但同样适用于Rails 5.1。
一个Rails加密消息的解剖
让我从稍微修改您提供的代码开始。唯一的更改是将密码和盐预设为静态值并打印大量调试信息:
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(详见源代码)来推导密钥,但可以通过使用-pbkdf2-md sha1选项来配置使用pbkdf2_hmac_sha1算法。可以使用-iter选项设置迭代次数。

如何在openssl中解密Rails加密消息

因此,我们现在已经有足够的信息来尝试在openssl中解密Rails加密消息。

首先,我们必须再次解码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 将用于生成加密密钥的密码设置为 "password"
  • -pbkdf2-md sha1 设置与 Rails 使用的相同的密钥导出算法 (pbkdf2_hmac_sha1)
  • -iter 65536 设置与 Rails 使用的相同次数的密钥导出迭代
  • -a 允许使用 Base64 编码的加密数据 - 无需处理文件中的原始字节

默认情况下,openssl 从 STDIN 读取数据,因此我们只需要使用 echo 将格式正确的加密数据传递给 openssl

调试

如果您在使用 openssl 解密时遇到任何问题,可以在命令行中添加 -P 参数,该参数输出有关密码 / 密钥参数的调试信息:

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

saltkeyiv的值必须对应于在上面encrypt_text方法中打印的调试值。如果它们不同,那么您就知道您做错了些什么...

现在,我猜当您尝试在go中解密消息时,可能会遇到类似的问题,但我认为您现在有了一些很好的指针可以开始了。


哇塞,非常感谢详细的描述。真的很感激。不过我有一个问题,序列化数据是如何改变数据的? - defiant
1
只需自行查看:打开Rails控制台,比较“一些文本”与Marshal.dump(“一些文本”)。您将看到后者在输出中有一些更多的字节,这些字节表示数据类型(可能)。请参阅[文档](https://ruby-doc.org/core-2.3.0/Marshal.html)。我还发现了[此加载程序](https://github.com/dozen/ruby-marshal),可以在Go中加载Ruby封送数据,因此,如果需要加密比字符串更复杂的任何内容,则实际上可能需要这样做(并进行序列化)。 - Matouš Borák
1
谢谢,现在我知道额外的信息表示数据类型。我只是好奇一旦数据被序列化,这些额外的字符是如何出现的。 - defiant
是的,编组数据格式是内部格式,文档不太完整,但很可能表示数据类型。这使得序列化程序支持任何类型的数据,而不仅仅是字符串。 - Matouš Borák

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接