尝试使用 openssl/golang 解密已在 Rails 中加密的字符串

def*_*ant 4 ruby openssl ruby-on-rails encryption-symmetric

我正在尝试解密已在我的 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
Run Code Online (Sandbox Code Playgroud)

现在的问题是我无法使用 openssl 解密它。它只是显示了糟糕的幻数。一旦我在 open 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
Run Code Online (Sandbox Code Playgroud)

这只是显示了糟糕的幻数

Bor*_*aMa 5

除非您了解并处理两个系统如何进行加密的许多复杂细节,否则尝试解密在不同系统中加密的数据是行不通的。尽管 Rails 和openssl命令行工具都在底层使用 OpenSSL 库进行加密操作,但它们都以自己独特的方式使用它,不能直接互操作。

\n\n

如果您仔细观察这两个系统,您会发现例如:

\n\n
    \n
  • Rails 消息加密器不仅对消息进行加密,还对其进行签名
  • \n
  • Rails 加密器用于Marshal序列化输入数据
  • \n
  • openssl enc工具期望加密数据采用带有Salted__<salt>标头的不同文件格式(这就是为什么您会收到错误的幻数消息openssl
  • \n
  • openssl工具必须正确配置为使用与 Rails 加密器和密钥生成器相同的密码,因为openssl默认值与 Rails 默认值不同
  • \n
  • 自 Rails 5.2 以来,默认密码配置发生了显着变化。
  • \n
\n\n

有了这些一般信息,我们就可以看一个实际的例子。它在 Rails 4.2 中进行了测试,但在 Rails 5.1 之前应该同样有效。

\n\n

Rails 加密消息剖析

\n\n

让我从您提供的稍微修改过的代码开始。唯一的变化是将password和预设salt为静态值并打印大量调试信息:

\n\n
def encrypt_text(text_to_encrypt)\n  password = "password" # the password to derive the key\n  salt = "saltsalt" # salt must be 8 bytes\n\n  key = ActiveSupport::KeyGenerator.new(password).generate_key(salt, 32)\n\n  puts "salt (hexa) = #{salt.unpack(\'H*\').first}" # print the saltin HEX\n  puts "key (hexa) = #{key.unpack(\'H*\').first}" # print the generated key in HEX\n\n  crypt = ActiveSupport::MessageEncryptor.new(key)\n  output = crypt.encrypt_and_sign(text_to_encrypt)\n  puts "output (base64) = #{output}"\n  output\nend\n\nencrypt_text("secret text")\n
Run Code Online (Sandbox Code Playgroud)\n\n

当您运行此命令时,您将得到类似于以下输出的内容:

\n\n
salt (hexa) = 73616c7473616c74\nkey (hexa) = 196827b250431e911310f5dbc82d395782837b7ae56230dce24e497cf07b6518\noutput (base64) = SGRTUXYxRys1N1haVWNpVWxxWTdCMHlyMk15SnQ0dWFBOCt3Z0djWVdBZz0tLTkrd1hBNWJMVm9HcnptZ3loOG1mNHc9PQ==--80d091e8799776113b2c0efd1bf75b344bf39994\n
Run Code Online (Sandbox Code Playgroud)\n\n

最后一行(encrypt_and_sign方法的输出)是由两个部分分隔的组合--(请参阅源代码):

\n\n
    \n
  1. 加密消息(Base64 编码)和
  2. \n
  3. 消息签名(Base64 编码)。
  4. \n
\n\n

签名对于加密并不重要,所以让我们看一下第一部分 - 让我们在 Rails 控制台中对其进行解码:

\n\n
> Base64.strict_decode64("SGRTUXYxRys1N1haVWNpVWxxWTdCMHlyMk15SnQ0dWFBOCt3Z0djWVdBZz0tLTkrd1hBNWJMVm9HcnptZ3loOG1mNHc9PQ==")\n=> "HdSQv1G+57XZUciUlqY7B0yr2MyJt4uaA8+wgGcYWAg=--9+wXA5bLVoGrzmgyh8mf4w=="\n
Run Code Online (Sandbox Code Playgroud)\n\n

您可以看到解码后的消息再次由两个 Base64 编码部分组成,两个部分之间用--(参见源代码)分隔:

\n\n
    \n
  1. 加密消息本身
  2. \n
  3. 加密中使用的初始化向量
  4. \n
\n\n

Rails 消息加密器aes-256-cbc默认使用密码(请注意,自 Rails 5.2 以来,这已发生变化)。该密码需要一个初始化向量,该向量由 Rails 随机生成,并且必须存在于加密输出中,以便我们可以将其与密钥一起使用来解密消息。

\n\n

此外,Rails 不会将输入数据加密为简单的纯文本,而是将数据的序列化版本加密,Marshal默认情况下使用序列化程序(源代码)。如果我们使用 openssl 解密此类序列化值,我们仍然会得到初始纯文本数据的稍微乱码(序列化)版本。这就是为什么在 Rails 中加密数据时禁用序列化会更合适。这可以通过将参数传递给加密方法来完成:

\n\n
salt (hexa) = 73616c7473616c74\nkey (hexa) = 196827b250431e911310f5dbc82d395782837b7ae56230dce24e497cf07b6518\noutput (base64) = SGRTUXYxRys1N1haVWNpVWxxWTdCMHlyMk15SnQ0dWFBOCt3Z0djWVdBZz0tLTkrd1hBNWJMVm9HcnptZ3loOG1mNHc9PQ==--80d091e8799776113b2c0efd1bf75b344bf39994\n
Run Code Online (Sandbox Code Playgroud)\n\n

重新运行代码会产生比之前版本稍短的输出,因为加密数据现在尚未序列化:

\n\n
salt (hexa) = 73616c7473616c74\nkey (hexa) = 196827b250431e911310f5dbc82d395782837b7ae56230dce24e497cf07b6518\noutput (base64) = SUlIWFBjSXRUc0JodEMzLzhXckJzUT09LS1oZGtPV1ZRc2I5Wi8zOG01dFNOdVdBPT0=--58bbaf983fd20459062df8b6c59eb470311cbca9\n
Run Code Online (Sandbox Code Playgroud)\n\n

最后,我们必须了解一些有关加密密钥派生过程的信息。消息来源告诉我们,KeyGenerator 使用迭代pbkdf2_hmac_sha1算法2**16 = 65536从密码/秘密中导出密钥。

\n\n

openssl加密消息剖析

\n\n

现在,侧面需要进行类似的调查,openssl以了解其解密过程的细节。首先,如果您使用该工具加密任何内容openssl enc,您会发现输出具有不同的格式

\n\n
Salted__<salt><encrypted_message>\n
Run Code Online (Sandbox Code Playgroud)\n\n

它以Salted__魔术字符串开头,然后是(十六进制形式),最后是加密数据。为了能够使用此工具解密任何数据,我们必须将加密数据转换为相同的格式。

\n\n

openssl工具默认使用EVP_BytesToKey(参见源代码)来派生密钥,但可以配置为使用pbkdf2_hmac_sha1使用-pbkdf2-md sha1选项的算法。可以使用该选项设置迭代次数-iter

\n\n

如何解密 Rails 加密的消息openssl

\n\n

所以,最后我们有了足够的信息来实际尝试解密openssl.

\n\n

首先,我们必须再次解码 Rails 加密输出的第一部分,以获得加密数据和初始化向量:

\n\n
> Base64.strict_decode64("SGRTUXYxRys1N1haVWNpVWxxWTdCMHlyMk15SnQ0dWFBOCt3Z0djWVdBZz0tLTkrd1hBNWJMVm9HcnptZ3loOG1mNHc9PQ==")\n=> "HdSQv1G+57XZUciUlqY7B0yr2MyJt4uaA8+wgGcYWAg=--9+wXA5bLVoGrzmgyh8mf4w=="\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在让我们将 IV(第二部分)转换为十六进制字符串形式,因为这是openssl需要的形式:

\n\n
  # crypt = ActiveSupport::MessageEncryptor.new(key)\n  crypt = ActiveSupport::MessageEncryptor.new(key, serializer: ActiveSupport::MessageEncryptor::NullSerializer)\n\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在我们需要获取 Rails 加密的数据并将其转换为openssl可识别的格式,即在其前面添加魔术字符串和盐,然后再次对其进行 Base64 编码:

\n\n
salt (hexa) = 73616c7473616c74\nkey (hexa) = 196827b250431e911310f5dbc82d395782837b7ae56230dce24e497cf07b6518\noutput (base64) = SUlIWFBjSXRUc0JodEMzLzhXckJzUT09LS1oZGtPV1ZRc2I5Wi8zOG01dFNOdVdBPT0=--58bbaf983fd20459062df8b6c59eb470311cbca9\n
Run Code Online (Sandbox Code Playgroud)\n\n

最后,我们可以构造openssl解密数据的命令:

\n\n
Salted__<salt><encrypted_message>\n
Run Code Online (Sandbox Code Playgroud)\n\n

瞧\xc3\xa1,我们成功解密了初始消息!

\n\n

参数openssl如下:

\n\n
    \n
  • -aes-256-cbc设置与 Rails 用于加密的密码相同的密码
  • \n
  • -d代表解密
  • \n
  • -iv以十六进制字符串形式传递初始化向量
  • \n
  • -pass pass:password将用于导出加密密钥的密码设置为“password”
  • \n
  • -pbkdf2-md sha1设置与 Rails 使用的相同的密钥派生算法 ( pbkdf2_hmac_sha1)
  • \n
  • -iter 65536设置与 Rails 中相同的密钥派生迭代次数
  • \n
  • -a允许使用 Base64 编码的加密数据 - 无需处理文件中的原始字节
  • \n
\n\n

默认情况下openssl从 STDIN 读取,因此我们只需使用 echo 将加密数据(以正确的格式)\xc2\xa0 传递给即可openssl

\n\n

调试

\n\n

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

\n\n
> Base64.strict_decode64("SUlIWFBjSXRUc0JodEMzLzhXckJzUT09LS1oZGtPV1ZRc2I5Wi8zOG01dFNOdVdBPT0=")\n=> "IIHXPcItTsBhtC3/8WrBsQ==--hdkOWVQsb9Z/38m5tSNuWA=="\n
Run Code Online (Sandbox Code Playgroud)\n\n

、和值必须与上面打印salt的方法中原始代码打印的调试值相对应。如果它们不同,你就知道你做错了什么......keyivencrypt_text

\n\n

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

\n